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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Each release of Research-Kit, newest first. The version is `KIT_VERSION` in

## Unreleased

- `collect-remote --out`, `--workflow` and `--ref` given no value are refused (`--<name>
needs a value`, exit 3) before anything is created or dispatched (#270). The flag parser
reads a bare flag as `true`, and these three were not among the checked values: a bare
`--out` became the folder `true`, created in the working directory before the token was
even asked for, and with a token a bare `--workflow` or `--ref` would have dispatched a
run against the literal file or branch "true". They now go through `checkFlagValues`
with the five values the workflow checks.

- `decompose --max-scrapes` and `--limit` given no value are refused (`--<name> needs a
value`) instead of becoming 1: the flag parser reads a bare flag as `true`, and the
CLI's own `Number()` check let it through, so `--dry-run --max-scrapes` reported "scrape
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ is an open decision (2026-09-30):
| `bin/measure.mjs` | The judge-free citation report for the current project (ADR-0089); `--json` for machines. A report, never a gate; exit 2 outside a research project. | ADR-0089 |
| `bin/export-warc.mjs` | The corpus as one WARC 1.1 `.warc.gz` for archive tools (ADR-0090); `--out` (default `research-corpus.warc.gz`), written whole with `writeBytes` after `readWarc` re-reads it; a refused write is named by `writeFailure`, exit 2 (2026-09-29). A copy, never a gate; exit 2 outside a research project. | ADR-0090 |
| `bin/disclosure.mjs` | Probes a run and reports what a stranger can read. Calls `honourEnvProxy()` before its first request (ADR-0047). Exit codes ARE the finding, so it can gate something later: `0` shape only, `1` content readable, `2` subject leaked, `3` could not measure - which includes any probe that never reached GitHub, unless an exposure was measured anyway (`exitCode` in the lib). An unknown flag matching token/auth/key/secret is refused with the reason - authenticating this probe would answer a different question. | ADR-0035 |
| `bin/collect-remote.mjs` | The one command an agent or a desktop app needs: dispatch the collector on GitHub, learn which run it started, wait, download, unwrap, and validate with the same code a human would run. Calls `honourEnvProxy()` before its first request, so a proxy-only network can dispatch at all (ADR-0047). Prints the run id **before** it waits, so a caller that dies mid-wait can still find its run - and a caller that never sees that line knows the dispatch itself did not land. **No `--token` flag**, and an unknown flag matching token/auth/key/secret/pat is refused with the reason rather than the spelling. Exit codes separate the four things a caller does differently about: `0` valid (still not authorized to build), `1` invalid package, `2` run failed or incomplete, `3` could not start, `4` dispatched and still running with the run id on stdout. An `--out` it cannot write is refused (exit 3, `OUT_DIR`) before the token is read or a run dispatched (2026-09-27); `--no-wait` downloads nothing and skips the check. Each failed poll the watch survives is printed with its number (2026-10-01). `--run-id <n>` picks up a run already dispatched - after `--no-wait`, a timeout (exit 4) or a lost connection - waits, downloads and judges it, and refuses `--topic` beside it (2026-10-01). The remedies and exit 4 had pointed at that flag while it did not exist, so the only way back to a run was to pay for another. A failure that names no way out now says how to pick the run up. | ADR-0033 |
| `bin/collect-remote.mjs` | The one command an agent or a desktop app needs: dispatch the collector on GitHub, learn which run it started, wait, download, unwrap, and validate with the same code a human would run. Calls `honourEnvProxy()` before its first request, so a proxy-only network can dispatch at all (ADR-0047). Prints the run id **before** it waits, so a caller that dies mid-wait can still find its run - and a caller that never sees that line knows the dispatch itself did not land. **No `--token` flag**, and an unknown flag matching token/auth/key/secret/pat is refused with the reason rather than the spelling. Exit codes separate the four things a caller does differently about: `0` valid (still not authorized to build), `1` invalid package, `2` run failed or incomplete, `3` could not start, `4` dispatched and still running with the run id on stdout. An `--out` it cannot write is refused (exit 3, `OUT_DIR`) before the token is read or a run dispatched (2026-09-27); `--no-wait` downloads nothing and skips the check. Each failed poll the watch survives is printed with its number (2026-10-01). `--run-id <n>` picks up a run already dispatched - after `--no-wait`, a timeout (exit 4) or a lost connection - waits, downloads and judges it, and refuses `--topic` beside it (2026-10-01). The remedies and exit 4 had pointed at that flag while it did not exist, so the only way back to a run was to pay for another. A failure that names no way out now says how to pick the run up. A bare `--out`, `--workflow` or `--ref` is refused as `--<name> needs a value` (exit 3) beside the five checked values (2026-10-11, #270): the parser reads a bare flag as `true`, so `--out` had become the folder `true`, created before the token was asked for, and `--workflow` or `--ref` would have dispatched against the literal name "true". | ADR-0033 |
| `bin/researcher-release.mjs` | The release CLI: `validate`, `conform`, `fi-validate`. **A usage error returns 2 from every subcommand**, and a status maps to its own code — `PASS` 0, `FAIL`/`REOPEN` 1, `INCOMPLETE` 2, `BLOCKED` 3. `4` is the unrecognised-status fallback and never a usage error, so a caller can distinguish "did not run" from "ran and produced something unknown". `--help` prints the usage to stdout and exits 0 (2026-09-27), as do `path-authority`, `ledger-conformance`, `fi-sidecar-conformance` and `artifact`. All five had exited 2. `--report` is written through `writeText` (2026-09-28, ADR-0079), whole or not at all. `fi-validate --report` names a report path the environment refuses through `writeFailure` and exits 2 (2026-09-30); it had crashed with a stack and exit 1, its code for FAIL. `test/hardening.test.mjs` now requires every `bin/` command that imports a writer from `lib/` to call `writeFailure`, or to name the handler that makes it unnecessary. | — |
| `bin/panel.mjs` | Starts the desktop panel (`lib/panel.mjs`) and opens the browser at its address (`explorer.exe` on Windows, `open`, `xdg-open`; `--no-open` prints it only). Flags: `--project <folder>`, `--port <n>`, `--no-open`. Stops on Ctrl+C. A Windows desktop shortcut is a target the operator writes (README), not an installed launcher (ADR-0050). | ADR-0147 |
| `release-validator.mjs` | Read-only R28–R32 release evidence validation: envelope/predecessor/role/visibility/promotion checks, ledger anchoring, and deterministic `PASS`/`INCOMPLETE`/`FAIL`/`REOPEN` reduction. It never rewrites records, pointers, registries, or role rosters. **Also the facade** over the four primitives below: all 21 of its original exports are re-exported unchanged, so the eighteen files importing from it were not touched. 1324 → ~1010 lines. Record ids and file lists sort by `compareText` (2026-10-01), the order the Python runner uses. `validateRelease` hands an explicit `genesisPath` and `checkpointsDir` to the ledger check, holds them to the ledger's below-the-root rule, and refuses either without a ledger (2026-10-11): until then both were dropped unread. | R28–R32 validator contract |
Expand Down
2 changes: 1 addition & 1 deletion research-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ node research-kit/bin/selftest.mjs # all of it
node research-kit/bin/selftest.mjs gate hook # just these files
```

1786 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
1787 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
the assertions settled (ADR-0021), and each test is raced against a watchdog
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).

Expand Down
5 changes: 5 additions & 0 deletions research-kit/bin/collect-remote.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,17 @@ if (missing.length) {
// Every value the workflow checks is checked here first, against the same ranges and choices
// (collect.yml): `--max-pages 8o` was dispatched as it was and the paid run failed on the
// runner (found 2026-09-29, Arena break test) - on the one flag that bounds spend.
// --out, --workflow and --ref take any string, but not NO string (found 2026-10-11, #270):
// parseFlags reads a bare flag as `true`, so a bare --out became the folder "true" - created
// below before the token was asked for - and a bare --workflow or --ref would have dispatched
// against the literal file or branch "true".
checkFlagValues(flags, {
'max-pages': { int: true, min: 1, max: 25 },
depth: { choices: ['probe', 'quick', 'normal'] },
runner: { choices: ['ubuntu-latest', 'windows-latest'] },
'search-transport': { choices: ['auto', 'serpapi', 'firecrawl-cli'] },
timeout: { int: true, min: 1 },
out: {}, workflow: {}, ref: {},
}, { exit: EXIT.CANNOT_START });

// The caller's own typo is refused before anything else is asked of them - a missing
Expand Down
14 changes: 14 additions & 0 deletions research-kit/test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,20 @@ test('collect-remote refuses a bad --max-pages, --depth, --runner or --timeout b
}
});

// Found 2026-10-11 (#270): parseFlags reads a bare flag as `true`, and --out, --workflow and
// --ref were not among the checked values. A bare --out became `path.resolve("true")`, and
// usableOutDir CREATED that folder before the token was even asked for; with a token, a bare
// --workflow or --ref would have dispatched a run against the literal file "true".
test('collect-remote refuses a bare --out, --workflow or --ref before it creates or dispatches anything', () => {
for (const flag of ['--out', '--workflow', '--ref']) {
const root = project();
const r = run('collect-remote.mjs', ['--repository', 'o/r', '--topic', 't', flag], { root });
assert.equal(r.status, 3, `${flag}: ${r.all}`);
assert.match(r.err.split('\n')[0] ?? '', new RegExp(`^${flag} needs a value`), `${flag} was not refused by name first:\n${r.all}`);
assert.ok(!fs.existsSync(path.join(root, 'true')), `${flag}: a folder named "true" was created in the project`);
}
});

// Found 2026-10-01: lib/dispatch.mjs told the caller to "retry with --run-id <n>", exit 4 said
// "the run id is on stdout", and collect-remote had no --run-id - it refused it as an unknown
// option. A watch that ended (a timeout, a dropped connection, a closed laptop) could not be
Expand Down
Loading