Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ reviews:
Preserve unrelated editor tabs, dirty buffers, breakpoints, and processes.
Review cross-host assumptions against the cycle's declared support matrix.

- path: 'extension/src/**/{ownedNodeServerRun,vscodeNodeServerDebugger,launchNodeServer,confirmLaunchedGroup,reapNodeServerOrphans,signalProcessGroup,isProcessGroupGone,isGroupCertainlyGone}.ts'
instructions: |
Process signalling: every signal to a pid or a process group follows an
identity verdict (the launched leader's start time) taken within the same
bound as the signal. A signal-0 probe of a group proves only that some group
holds the id now, never that it is the launched one: a group id can be reused
between two probes. A signal sent after the operation that owns it has
returned (a close, a release, a sweep) is a defect, and so is a probe or
identity read that pushes a signal past its stated bound. Name the event
sequence that reaches each finding.

- path: 'extension/src/ui/**'
instructions: |
Follow PRODUCT_GOAL.md's design target and the approved design, as amended by the required reads and
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
package_json_file: extension/package.json
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.21.0
node-version-file: .node-version
cache: pnpm
cache-dependency-path: |
extension/pnpm-lock.yaml
Expand Down
5 changes: 5 additions & 0 deletions .gstack/pregate.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
".github/reader-dom/package-lock.json": [
"extension/scripts/tests/workflowPins.test.mjs"
],
"extension/scripts/gate.mjs": ["extension/scripts/tests/gate.test.mjs"],
"extension/scripts/ci-failures.mjs": [
"extension/scripts/tests/ciFailures.test.mjs"
],
"extension/.node-version": ["extension/scripts/tests/gate.test.mjs"],
"extension/scripts/checkHealth.mjs": [
"extension/scripts/tests/health.test.mjs"
],
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.21.0
24.22.1
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Driving agent owns state, cycle selection, completion claims; GStack supplies wo
- A large cycle may use up to three PRs (`cycle100-pt1`, `cycle100-pt2`, …), each passing reviews, CI and gate alone, merged in order; the Issue closes after the last.
- Codex automatic review off (no Codex cloud step). No `@coderabbitai ignore` in PR bodies (owner, 2026-10-05).
- Hosted CodeRabbit off from Cycle 0057 (owner, 2026-10-27: 「次のサイクルからGitHubのコード、Rabbitのレビューはトリガーさせないでください。」): `.coderabbit.yaml` keeps `reviews.auto_review.enabled: false` (CodeRabbit reads the PR branch's file); never post `@coderabbitai review`.
- Outside review early (owner, 2026-10-28): `/ship` Step 9 pass 1 also runs Step 11's Codex structured review (`codex review --base main`) on the whole diff, its findings part of pass 1; a plan that changes process signalling, process identity or a stop/kill path also gets a Codex review before implementation. A fix-range pass (pass 2 on) under 300 changed lines dispatches the red team and the specialists whose area the range changes.

**Delivery parameters.**

Expand Down Expand Up @@ -143,6 +144,7 @@ Driving agent owns state, cycle selection, completion claims; GStack supplies wo
**CodeRabbit and OCR.**

- [.coderabbit.yaml](.coderabbit.yaml) = policy + exclusions: keep its guidelines current; validate with `coderabbit config validate .coderabbit.yaml`; pass it via `--config` beside extra context.
- CLI 0.9.1 (latest on 2026-10-28) exits 1 on every file with `unknown keyword: "x-undocumented"`, a defect of its own schema: record that line as the validation result; a review with `-c` still applies the file.
- Required: local CodeRabbit CLI, findings triage, CI, `open-code-review-delegate` (OCR picks files and rules, the host reviews; each eligible file reviewed or skipped with a reason; default exclusions reported apart; no independent external review).
- Review against the implementation base; record completion, source cut, covered files, findings.
- Hosted CodeRabbit is not triggered (owner, 2026-10-27; [Delivery](#delivery)): merge waits for no hosted review or approval. A hosted review that still appears is reported in the PR; its findings follow the cycle rule like the local review's. Run the local CodeRabbit CLI on the PR's final range before merge.
Expand Down Expand Up @@ -220,11 +222,13 @@ Standing rules, stated as rules (no decision log); delivery chain and ship limit

## Commands and checks

Node and pnpm as pinned in the extension manifest and CI; run from the repo root.
Node as `.node-version` pins it (mirrored in `extension/.node-version`, since a version manager's shim reads the nearest package's `engines` there) and pnpm as the extension's `packageManager` pins it; put that Node's own `bin` first in `PATH`; run from the repo root.

**Gate and focused runs.**

- `pnpm --dir extension test` = build + Node's isolated runner (files parallel, tests in a file serial; never same-process concurrency globally: workspace cases mock shared prototypes and timers). Full gate before delivery. `pnpm --dir extension test:package` = package tests.
- Exact-HEAD gate: `pnpm --dir extension gate`: the fifteen delivery checks in order (static checks, the suite, the five Reader DOM checks, package tests, measured health, the compaction-hook test), one log each under `artifacts/gate/<HEAD>`; it refuses a Node or pnpm off the pins and a checkout with changes, and exits 0 only when every check passed on an unmoved HEAD.
- `pnpm --dir extension test` = build + Node's isolated runner (files parallel, tests in a file serial; never same-process concurrency globally: workspace cases mock shared prototypes and timers). `pnpm --dir extension test:package` = package tests.
- CI failures: `node extension/scripts/ci-failures.mjs <PR>` (or `--run <id>`) prints each failed job's failing tests with their first error line, also while the run goes on.
- Focused replay/workspace work: `pnpm --dir extension build` once, then `node --test extension/dist/tests/studyWorkspace*.test.js extension/dist/tests/playbackController.test.js` (every partition; `studyWorkspace.test.js` alone is one); reuse a current build.
- `studyWorkspaceHarness.ts` = fixtures only; each harness makes its own temp repo. Related race steps in one test; await each observable result before the next action. Keep real timeouts, production deadlines and assertions.

Expand Down Expand Up @@ -287,6 +291,7 @@ Node and pnpm as pinned in the extension manifest and CI; run from the repo root

## Native QA

- Observing a native trial (state checks, screenshots, evidence stills): [docs/agents/native-trial-observation.md](docs/agents/native-trial-observation.md).
- UI changes: view the PNG again; compare real screenshots with its region at the normal desktop baseline; record differences and interaction.
- Motion needs recorded interaction + inspected frames; generated pixels or renderer assertions prove no native behavior.

Expand Down
29 changes: 29 additions & 0 deletions docs/agents/native-trial-observation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Native trial observation

Read before driving a native trial in the Cua Space (`local:codebook-qa`) or on a desktop. A `computer_screenshot` answers about 600,000 characters; in Cycles 0056 to 0058 screenshots were 85% of all tool output, and they forced most of the context compactions. Look with **text** first and spend **pictures** on visual evidence.

## Text: check state

Every step that only needs to know what is there (a window, a dialog, a label, a value, a process, a file) is checked with text:

- **Windows and focus**: `computer_window_list` (filter with `app`), `computer_get_current_window`.
- **Controls and labels**: `computer_get_accessibility_tree` with the window's `window_id` and a `max_depth`; it names buttons, dialog titles, picker entries and the chat's input text.
- **Acting on a control**: `computer_accessibility_act` with the tree's `snapshot_id` and `element_id` (`press`, `focus`, `set_value`); it needs no coordinates, so no screenshot to find them.
- **Processes, ports, files and logs in the Space**: `space_bash`, for example `lsof -nP -iTCP:4100 -sTCP:LISTEN`, `pgrep -fl 'Visual Studio Code'`, `osascript -e 'tell application "System Events" to get name of every window of process "Code"'`, or a `tail` of the file a step writes.

After an action, verify its result the same way: the tree, the process or the file shows it.

## Pictures: visual evidence

A claim about what the learner sees (layout against the v9 PNG, rendering, a frame of motion) needs pixels. Capture a region in the Space and shrink it before reading it:

```sh
screencapture -x -R<x>,<y>,<width>,<height> /tmp/shot.png
sips -s format jpeg -s formatOptions 70 -Z 1000 /tmp/shot.png --out /tmp/shot.jpg
```

Then `download` `/tmp/shot.jpg` and Read it.

- **Evidence stills**: keep the full-size PNG as the evidence file (downloaded, SHA-256 on both sides, as AGENTS.md _Native QA_ says) and Read only the reduced copy.
- **Coordinates**: use `computer_screenshot` when a click needs coordinates the accessibility tree cannot give (a canvas, a webview without accessible names); its pixel space is the one clicks use.
- **Motion**: recordings and their frames follow AGENTS.md _Native QA_; Read reduced frames (`ffmpeg … -vf scale=1000:-1`).
1 change: 1 addition & 0 deletions extension/.node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.22.1
4 changes: 2 additions & 2 deletions extension/fallow-health-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
"maxCrap": 30
},
"producers": {
"node": "24.21.0",
"node": "24.22.1",
"c8": "12.0.0",
"v8ToIstanbul": "9.3.0",
"fallow": "3.34.1"
},
"settings": {
".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f",
".node-version": "d9f318747a126ce2247661af67852b185def7789492eabfad7bba322be087c33",
"extension/.c8rc.json": "3b45447f2b721137e6f863dc3136c5aef26a14710dd6bdfb09756fcc5077ea6a",
"extension/.fallowrc.json": "de0d5f49a2c14a06a32de72184a95b2b90089318e3be6d434a212a2029fd0571"
},
Expand Down
1 change: 1 addition & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@
"format": "cd .. && prettier --write .",
"format:check": "cd .. && prettier --check .",
"health": "node scripts/checkHealth.mjs",
"gate": "node scripts/gate.mjs",
"health:static": "fallow health --complexity --max-crap inf --baseline-mode identity --fail-on-issues --baseline fallow-health-baseline.json",
"dupes": "fallow dupes --fail-on-issues --baseline fallow-dupes-baseline.json",
"dead-code": "fallow dead-code --fail-on-issues --baseline fallow-dead-code-baseline.json",
Expand Down
6 changes: 3 additions & 3 deletions extension/scripts/checkHealth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const TAP_SUMMARY_LINE = /^# (tests|pass|fail|cancelled|skipped|todo) (\d+)$/gm
/** How many names a refusal lists before "and N more". */
const SHOWN_NAME_LIMIT = 5

/** Hashes a text file with CRLF read as LF, so a Windows checkout names the same bytes as the reference. @example await hashText('/repo/.node-version') // => '73fb1b61…' */
/** Hashes a text file with CRLF read as LF, so a Windows checkout names the same bytes as the reference. @example await hashText('/repo/.node-version') // => 'd9f31874…' */
async function hashText(path) {
const text = (await readFile(path, 'utf8')).replaceAll('\r\n', '\n')
return createHash('sha256').update(text).digest('hex')
Expand Down Expand Up @@ -116,7 +116,7 @@ async function readReference() {
return reference
}

/** Reads the version of every producer of the measured run as installed here. @example await readProducers() // => { node: '24.21.0', c8: '12.0.0', v8ToIstanbul: '9.3.0', fallow: '3.34.1' } */
/** Reads the version of every producer of the measured run as installed here. @example await readProducers() // => { node: '24.22.1', c8: '12.0.0', v8ToIstanbul: '9.3.0', fallow: '3.34.1' } */
async function readProducers() {
const c8Manifest = requireFromExtension.resolve('c8/package.json')
const converterManifest = createRequire(c8Manifest).resolve(
Expand Down Expand Up @@ -196,7 +196,7 @@ function childEnvironment() {
* @param options - More `spawn` options; a `stdio` here replaces the default.
* @returns `{ code, signal, stdout }` once the child has exited; `stdout` holds its output only when `options.stdio` pipes it.
* @example
* await runChild(process.execPath, ['--version'], { stdio: ['ignore', 'pipe', 2] }) // => { code: 0, signal: null, stdout: 'v24.21.0\n' }
* await runChild(process.execPath, ['--version'], { stdio: ['ignore', 'pipe', 2] }) // => { code: 0, signal: null, stdout: 'v24.22.1\n' }
*/
function runChild(command, arguments_, options = {}) {
return new Promise((resolve, reject) => {
Expand Down
158 changes: 158 additions & 0 deletions extension/scripts/ci-failures.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Prints the failing tests of a pull request's or a workflow run's failed CI jobs, also while the run is still going.
//
// node extension/scripts/ci-failures.mjs <pull request number>
// node extension/scripts/ci-failures.mjs --run <run id>
//
// For each failed job: its name and URL, then each failing test of the runner's "failing tests" block with the first
// error line under it; a failed job without that block prints its last lines instead. Needs an authenticated `gh`
// (`GH_TOKEN` is passed through). Exit 0 after printing (also when nothing failed), 2 on a bad argument or a `gh` failure.
import { execFileSync } from 'node:child_process'
import { parseArgs } from 'node:util'

import { isRunAsScript } from './utils/isRunAsScript.mjs'

/** A GitHub Actions log line's timestamp prefix, as `gh api …/logs` writes it. */
const TIMESTAMP_PREFIX = /^\d{4}-\d\d-\d\dT[\d:.]+Z /
/** A terminal colour sequence, which the logs keep. */
const COLOUR_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
/** How many of a failed job's last lines it prints when the log has no "failing tests" block. */
const TAIL_LINES = 15

/**
* Reads the failing tests out of one CI job's log: every `✖ <name>` of each `✖ failing tests:` block Node's spec
* reporter ends a failed run with, with the first error line under it.
* Why: a sharded job prints thousands of lines, and `gh run view --log-failed` prints nothing while the run is in
* progress; the names and their first error line are what a fix starts from. Called by {@link main} for each failed job.
* @param log - The job's log as `gh api …/actions/jobs/<id>/logs` answers it (timestamps and colours included).
* @returns `{ name, error }` per failing test, in log order, each name once; `error` is `undefined` when no error line follows.
* @example
* failedTestsOf('2026-10-28T00:00:00Z ✖ failing tests:\n2026-10-28T00:00:00Z ✖ adds (1.2ms)\n2026-10-28T00:00:00Z AssertionError [ERR_ASSERTION]: 1 !== 2')
* // => [{ name: 'adds', error: 'AssertionError [ERR_ASSERTION]: 1 !== 2' }]
*/
export function failedTestsOf(log) {
const lines = log
.split('\n')
.map((line) =>
line.replace(TIMESTAMP_PREFIX, '').replace(COLOUR_SEQUENCE, ''),
)
const failures = new Map()
let inBlock = false
let current
for (const line of lines) {
if (line.startsWith('✖ failing tests:')) {
inBlock = true
current = undefined
continue
}
// The block ends at the next summary or reporter line.
if (!inBlock || line.startsWith('ℹ ')) {
inBlock = false
continue
}
if (line.startsWith('✖ ')) {
const name = line.slice(2).replace(/ \([\d.]+m?s\)$/, '')
current = failures.get(name) ?? { name, error: undefined }
failures.set(name, current)
continue
}
// The first line naming an error under a test is its error line.
if (current && current.error === undefined && /Error\b/.test(line))
current.error = line.trim()
}
return [...failures.values()]
}

/** Runs `gh` and answers its stdout. @example gh(['api', 'user']) // => '{"login":…}' */
function gh(args) {
return execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 256 * 1024 * 1024,
})
}

/** Lists the workflow runs of a pull request's head commit. @example runsOfPullRequest(144) // => [7101, 7102] */
function runsOfPullRequest(number) {
const head = gh([
'pr',
'view',
String(number),
'--json',
'headRefOid',
'-q',
'.headRefOid',
]).trim()
return JSON.parse(
gh(['run', 'list', '--commit', head, '--json', 'databaseId']),
).map((run) => run.databaseId)
}

/**
* The command line: finds the failed jobs of the runs named, and prints each job's failing tests.
* @returns The exit code.
* @example main() // => 0
*/
function main() {
let parsed
try {
parsed = parseArgs({
options: { run: { type: 'string' } },
allowPositionals: true,
})
} catch (error) {
console.error(error.message)
return 2
}
const [pullRequest] = parsed.positionals
if ((pullRequest === undefined) === (parsed.values.run === undefined)) {
console.error('Name one pull request number, or one run with --run <id>.')
return 2
}
try {
const repository = gh([
'repo',
'view',
'--json',
'nameWithOwner',
'-q',
'.nameWithOwner',
]).trim()
const runs =
parsed.values.run === undefined
? runsOfPullRequest(pullRequest)
: [parsed.values.run]
let failedJobs = 0
for (const run of runs) {
const { jobs } = JSON.parse(
gh(['run', 'view', String(run), '--json', 'jobs']),
)
for (const job of jobs.filter((each) => each.conclusion === 'failure')) {
failedJobs += 1
console.log(`\n## ${job.name}\n${job.url}`)
// The logs keep the runners' colours, which `gh api` prints only when asked to; they are stripped before reading.
const log = gh([
'api',
'--allow-escape-sequences',
`repos/${repository}/actions/jobs/${job.databaseId}/logs`,
])
const failures = failedTestsOf(log)
// A job that failed outside the test runner (a refused health run, a build) shows its last lines.
if (failures.length === 0)
for (const line of log.split('\n').slice(-TAIL_LINES))
console.log(
line.replace(TIMESTAMP_PREFIX, '').replace(COLOUR_SEQUENCE, ''),
)
for (const failure of failures)
console.log(
`✖ ${failure.name}${failure.error ? `\n ${failure.error}` : ''}`,
)
}
}
if (failedJobs === 0) console.log('No failed job.')
return 0
} catch (error) {
console.error(`gh failed: ${error.message}`)
return 2
}
}

if (isRunAsScript(import.meta.url)) process.exitCode = main()
Loading