Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7b1ada1
F2: restore the owner's re-entry rule within the hook context limit
ryota-murakami Oct 21, 2026
d7860de
C49-1/C49-6: outcome, coverage, verdict, cause and preparation-check …
ryota-murakami Oct 21, 2026
f02572e
C49-1/C49-6: test-run outcomes, lifecycle, seal channel and refusals …
ryota-murakami Oct 21, 2026
0829c26
EN5: keep a failing test's exit facts without a report; remove the re…
ryota-murakami Oct 21, 2026
a70da9e
Tests for test-run lifecycle, seal channel, coverage cache, store sum…
ryota-murakami Oct 21, 2026
a1e47eb
C49-2/C49-6: NodeLearningSave, Open replay for test replays, Saved st…
ryota-murakami Oct 21, 2026
de2f569
Tests for Open replay of test replays, EN9 ownership, Saved studies r…
ryota-murakami Oct 21, 2026
2d02ec3
C49-7: skill 1.0.46 routes test questions to node-test and claims sav…
ryota-murakami Oct 21, 2026
fff2205
C49-10 (PR-A): documents for test replays and MCP API 1.8
ryota-murakami Oct 21, 2026
dc88318
Tests for the browser recording's seal-failure channel and Open retai…
ryota-murakami Oct 21, 2026
7b72317
Activation keeps working where the service or the window API is a sta…
ryota-murakami Oct 21, 2026
df5eaeb
Neutral node-test examples in two docstrings (owner's varied-question…
ryota-murakami Oct 21, 2026
a4e760f
Fallow: one read of a summary's cited learning material; the sealed e…
ryota-murakami Oct 21, 2026
7905beb
The node-test help's start example names its key as a placeholder; a …
ryota-murakami Oct 21, 2026
88e344d
F1: the four macOS-only test failures were test isolation; each test …
ryota-murakami Oct 21, 2026
63a35ea
Coverage audit tests (a failing exit, the Output channel sinks, a san…
ryota-murakami Oct 21, 2026
7437ca4
fix: pre-landing review fixes — a test run's Save item is drawn while…
ryota-murakami Oct 21, 2026
e309202
fix: pre-existing test failure in testShards.test.mjs (F7): the four …
ryota-murakami Oct 21, 2026
920fafe
Two workspace tests pass their start key through a named variable (th…
ryota-murakami Oct 21, 2026
ae5f310
fix: pre-landing review fixes, fixing cycle 2 — a stop during the fin…
ryota-murakami Oct 22, 2026
2c4cb6c
Merge remote-tracking branch 'origin/main' into codex/cycle-0049-test…
ryota-murakami Oct 22, 2026
4d9d592
fix: pre-landing review fixes, fixing cycle 3 — a learner who closes …
ryota-murakami Oct 22, 2026
a56b86c
fix: end a test run's stop when the debug adapter never answers, and …
ryota-murakami Oct 22, 2026
99cf083
test: coverage audit tests, repeat 2 (a stop while a test run prepare…
ryota-murakami Oct 22, 2026
13a768b
Merge remote-tracking branch 'origin/main' into codex/cycle-0049-test…
ryota-murakami Oct 22, 2026
7a161b4
chore: re-record test timings (EN20) for the merged tree, with the St…
ryota-murakami Oct 22, 2026
09fa794
chore: bump version and changelog (v0.2.33.0)
ryota-murakami Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions .codex/restore-product-context.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'

/** Supplies current repository instructions to Codex after compaction. Called by the SessionStart command hook. @example echo '{"hook_event_name":"SessionStart","source":"compact"}' | node .codex/restore-product-context.mjs */
// Claude Code injects hook context of up to 10,000 characters whole; longer text arrives only as a 2,000-character preview.
const MAX_CONTEXT_CHARS = 10_000
// The sections every restored context carries verbatim; the rest of AGENTS.md is read from disk.
const RESTORED_SECTIONS = ['## Mandatory entry', '## Owner directions in force']

/**
* Returns one `## ` section of AGENTS.md, from its heading to the next `## ` heading.
*
* Why: the restored context carries only the sections that order the re-entry and keep the owner's scope, so it stays within the hosts' context limits.
* Caller: {@link restoreProductContext}.
*
* @param instructions - The current AGENTS.md text.
* @param heading - The exact `## ` heading line.
* @returns The section text without trailing blank lines.
* @throws When the heading is missing, so that a renamed section never restores silently without it.
* @example
* sectionOf('# A\n\n## Mandatory entry\n\nRead.\n\n## Next\n', '## Mandatory entry') // => '## Mandatory entry\n\nRead.'
*/
function sectionOf(instructions, heading) {
const lines = instructions.split('\n')
const start = lines.indexOf(heading)
if (start === -1)
throw new Error(`AGENTS.md is missing its ${heading.slice(3)} section.`)
const next = lines.findIndex(
(line, index) => index > start && line.startsWith('## '),
)
return lines
.slice(start, next === -1 ? lines.length : next)
.join('\n')
.trimEnd()
}

/** Supplies the owner's re-entry rule and current repository instructions after compaction. Called by the SessionStart command hook. @example echo '{"hook_event_name":"SessionStart","source":"compact"}' | node .codex/restore-product-context.mjs */
async function restoreProductContext() {
let input = ''
// Hook events are small; reject oversized input before retaining an unbounded payload.
Expand All @@ -19,16 +51,28 @@ async function restoreProductContext() {
new URL('../AGENTS.md', import.meta.url),
'utf8',
)
if (!instructions.includes('## Mandatory entry'))
throw new Error('AGENTS.md is missing its Mandatory entry section.')
const context = [
const rule = [
'CODEBOOK: RESTORE PRODUCT CONTEXT AFTER COMPACTION',
`Repository root: ${root}`,
'Before continuing implementation or runtime trials, perform the Mandatory entry below again, as its step for a context compaction says, using the current files on disk: read the active cycle Issue (the open Issue with the label `cycle`) with its comments and the sections of PRODUCT_GOAL.md that the next task touches, and for UI work open the approved PNG with an image-viewing tool. A compacted summary or this reminder is not evidence of those reads or image inspection.',
"The owner's most important rule: before any other work after this compaction, whatever the task, read PRODUCT_GOAL.md in full, every line, and open the approved PNG (docs/designs/assets/codebook-product-spec-v9.png) with an image-viewing tool and check its identity against PRODUCT_GOAL.md's Design target; then read the active cycle Issue (the open Issue with the label `cycle`) with its comments and AGENTS.md in full from disk. A compacted summary or this reminder is not evidence of those reads or of the image inspection.",
'Reconcile the current source and affected acceptance gaps, then resume the latest owner-authorized task. Preserve pending owner instructions and any explicit pause; this reminder grants no additional work scope.',
'Current AGENTS.md follows; resolve its relative links from the repository root:',
instructions,
]
const sections = RESTORED_SECTIONS.map((heading) =>
sectionOf(instructions, heading),
)
const full = [
...rule,
'These AGENTS.md sections follow verbatim; read the whole file on disk and resolve its relative links from the repository root:',
...sections,
].join('\n\n')
// A grown AGENTS.md must never push the rule out of the inline context: keep the rule and point to the file.
const context =
full.length <= MAX_CONTEXT_CHARS
? full
: [
...rule,
`AGENTS.md's ${RESTORED_SECTIONS.map((heading) => heading.slice(3)).join(' and ')} sections exceed this reminder's ${MAX_CONTEXT_CHARS}-character limit; read them in AGENTS.md on disk now.`,
].join('\n\n')
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
Expand Down
133 changes: 118 additions & 15 deletions .codex/restore-product-context.test.mjs
Original file line number Diff line number Diff line change
@@ -1,52 +1,155 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'

const root = fileURLToPath(new URL('../', import.meta.url))
const script = fileURLToPath(
new URL('./restore-product-context.mjs', import.meta.url),
)
const compactEvent = JSON.stringify({
hook_event_name: 'SessionStart',
source: 'compact',
})

/** Reads one `## ` section of an AGENTS.md text the way a reader sees it: heading to the next `## ` heading. */
function agentsSection(text, heading) {
const lines = text.split('\n')
const start = lines.indexOf(heading)
const next = lines.findIndex(
(line, index) => index > start && line.startsWith('## '),
)
return lines.slice(start, next).join('\n').trimEnd()
}

/** Runs a copy of the hook beside a temporary AGENTS.md, as the repository layout places them. */
function runHookBeside(agentsText) {
const directory = mkdtempSync(join(tmpdir(), 'codebook-restore-'))
try {
mkdirSync(join(directory, '.codex'))
copyFileSync(
script,
join(directory, '.codex', 'restore-product-context.mjs'),
)
writeFileSync(join(directory, 'AGENTS.md'), agentsText)
return spawnSync(
process.execPath,
[join(directory, '.codex', 'restore-product-context.mjs')],
{ input: compactEvent, encoding: 'utf8' },
)
} finally {
rmSync(directory, { recursive: true, force: true })
}
}

test('compaction restores current goal-reading and image-viewing instructions from a repository subdirectory', () => {
// Value: protects=the owner's 2026-10-21 rule reaching the agent whole after every compaction (PRODUCT_GOAL.md in full and the PNG, then the Issue and AGENTS.md); fails_when=the hook prints all of AGENTS.md (Claude Code then injects only a 2,000-character preview), the rule leaves the first 2,000 characters, or a restored section differs from AGENTS.md; no_claim=that the agent actually performs the reads
test('compaction restores the re-entry rule and the Mandatory entry and Owner directions sections within the inline limit', () => {
// Arrange
const input = JSON.stringify({
hook_event_name: 'SessionStart',
source: 'compact',
})
const config = JSON.parse(
readFileSync(new URL('./hooks.json', import.meta.url), 'utf8'),
)
const agents = readFileSync(new URL('../AGENTS.md', import.meta.url), 'utf8')
// Act
const result = spawnSync(
'/bin/sh',
['-c', config.hooks.SessionStart[0].hooks[0].command],
{ cwd: new URL('../extension', import.meta.url), input, encoding: 'utf8' },
{
cwd: new URL('../extension', import.meta.url),
input: compactEvent,
encoding: 'utf8',
},
)
const output = JSON.parse(result.stdout)
const context = output.hookSpecificOutput.additionalContext
// Assert
assert.equal(result.status, 0)
assert.equal(result.stderr, '')
assert.equal(output.hookSpecificOutput.hookEventName, 'SessionStart')
assert.ok(output.hookSpecificOutput.additionalContext.includes(root))
assert.ok(context.includes(root))
assert.ok(
output.hookSpecificOutput.additionalContext.includes(
readFileSync(new URL('../AGENTS.md', import.meta.url), 'utf8'),
),
context.length <= 10_000,
`context has ${context.length} characters`,
)
assert.match(
output.hookSpecificOutput.additionalContext,
/open the approved PNG with an image-viewing tool/,
assert.ok(
context.indexOf(
'read PRODUCT_GOAL.md in full, every line, and open the approved PNG',
) < 2_000,
)
assert.ok(context.includes(agentsSection(agents, '## Mandatory entry')))
assert.ok(
context.includes(agentsSection(agents, '## Owner directions in force')),
)
assert.match(
output.hookSpecificOutput.additionalContext,
context,
/Preserve pending owner instructions and any explicit pause/,
)
assert.equal(output.continue, undefined)
assert.equal(output.decision, undefined)
})

// Value: protects=the rule surviving a grown AGENTS.md; fails_when=sections longer than the inline limit are printed anyway (the preview would cut the context) or the hook fails instead of restoring the rule; no_claim=the wording of the restored sections
test('an AGENTS.md whose restored sections outgrow the limit still restores the rule and points to the file', () => {
// Arrange
const oversized = [
'# codebook agent instructions',
'',
'## Mandatory entry',
'',
'Read the goal.\n'.repeat(400),
'## Owner directions in force',
'',
'Keep the owner scope.\n'.repeat(400),
'## Native QA',
'',
'Record windows.',
'',
].join('\n')
// Act
const result = runHookBeside(oversized)
const context = JSON.parse(result.stdout).hookSpecificOutput.additionalContext
// Assert
assert.equal(result.status, 0)
assert.ok(
context.length <= 10_000,
`context has ${context.length} characters`,
)
assert.match(
context,
/read PRODUCT_GOAL\.md in full, every line, and open the approved PNG/,
)
assert.match(
context,
/Mandatory entry and Owner directions in force sections exceed this reminder's 10000-character limit; read them in AGENTS\.md on disk now\./,
)
assert.ok(!context.includes('Read the goal.\nRead the goal.'))
})

// Value: protects=a renamed or removed section being noticed; fails_when=the hook restores a context without the Owner directions section; no_claim=which sections AGENTS.md should have
test('an AGENTS.md without the Owner directions section reports failure without inventing restored context', () => {
// Arrange
const withoutDirections =
'# codebook agent instructions\n\n## Mandatory entry\n\nRead the goal.\n'
// Act
const result = runHookBeside(withoutDirections)
// Assert
assert.equal(result.status, 1)
assert.equal(result.stdout, '')
assert.match(
result.stderr,
/AGENTS\.md is missing its Owner directions in force section\./,
)
})

test('startup, resume and other events do not add compaction instructions', () => {
// Arrange
const events = [
Expand Down
Loading