Files
msd-core/scripts/lint-docs-guard-registration.exempt-baseline.cjs
Tom Boucher dd4f179672 feat(#3970): per-task external-tracker content-resolution seam (#4000)
* feat(#3970): per-task external-tracker content-resolution seam

Implements ADR-3646 (Phase 1, #3970): a `<task tracker-id="...">` attribute
plus a new optional `taskContentResolver` capability-manifest field let a
capability resolve a task's action/verify/acceptance-criteria/read_first/done
content from an external issue tracker instead of PLAN.md's inline body.

- src/plan-document.cts: parses the `tracker-id` attribute into `PlanTask.trackerId`
- src/task-content-resolution.cts: new leaf module — split/find/build/resolve,
  with a hard-halt (throw) contract on ambiguous/failed/timeout/malformed
  resolution, never a silent fallback to possibly-stale inline text
- src/task-command-router.cts: new `task resolve-content --plan --task-id --raw`
  CLI verb wiring the module into a real process exit code
- gsd-core/bin/lib/capability-validator.cjs: validates the new
  `taskContentResolver` manifest field (feature-role only, cross-capability
  trackerPrefix uniqueness)
- gsd-core/workflows/execute-plan.md, gsd-core/references/loop-hook-dispatch.md,
  docs/reference/capability-manifest.md: wire the seam into the per-task loop
  and document it as a new `execute:task` point outside the existing
  contribution/step/gate vocabulary (unconditional in autonomous mode)

Closes #3970

* fix(#3970): gate checkpoint tasks out of content resolution, close trackerPrefix grammar parity gap, cover path-traversal guard

Standards/Spec code-review pass on the task-content-resolution seam (ADR-3646
Phase 1) found three defects:

1. execute-plan.md's task-content-resolution bullet fired on any
   tracker-id-bearing task with no check that it wasn't type="checkpoint:*",
   contradicting ADR-3646 Decision 1 (a checkpoint task must never enter
   resolve-content). plan-document.cts already parses trackerId: null
   unconditionally for checkpoint tasks; only the workflow prose needed the
   fix, so the bullet now explicitly excludes checkpoint tasks.

2. task-content-resolution.cts's parseResolverDeclaration accepted any
   non-empty trackerPrefix with no grammar check, while capability-
   validator.cjs's KEBAB_RE enforces kebab-case at install time — a
   Generative Fix Divergence gap. Added the same grammar (as a literal
   regex, documented as intentionally not shared across the .cts/.cjs build
   boundary) plus a parity test asserting the two surfaces agree across a
   valid/invalid trackerPrefix table.

3. task-command-router.cts's routeResolveContent path-traversal guard on
   --plan had zero test coverage. Added a test exercising a
   ../../../etc/passwit-shaped path and asserting the USAGE rejection names
   the offending path.

* fix(#3970): sanitize resolver diagnostics and cap resolver timeoutMs

Two findings caught by an isolated security-review pass on the task
content resolution seam:

- ResolverFailedError/ResolverMalformedOutputError embedded raw,
  unsanitized subprocess stderr/stdout (attacker/model-influenced via
  the tracker-id argv token) into .message. A hostile or buggy resolver
  could smuggle a newline plus a forged "Error: " line, or terminal
  escape sequences, into a diagnostic io.cjs's error() writes verbatim
  to stderr. Fixed at the constructor (task-content-resolution.cts) via
  io.cjs's existing formatDiagnosticToken(), so every caller of
  resolveTaskContent gets a safe .message by construction.

- capability-validator.cjs's validateTaskContentResolverFields had no
  upper bound on taskContentResolver.invoke.timeoutMs, letting a
  manifest declare an effectively unbounded value and defeat the
  "bounded subprocess" design intent. Added a 120000ms ceiling specific
  to this field, without touching the shared isPositiveIntegerMs()
  helper (still used unbounded by the reviewer lane's timeoutFloorMs
  and probe timeoutMs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#3970): fix gsd-test failures — stale prose allowlist line and stderr-vs-message assertion

gsd-test (remote dockerized matrix) came back red with 5 failures on this
PR; all five are real defects, fixed here.

- tests/no-bare-gsd-tools-command-position.test.cjs: PROSE_ALLOWLIST's
  execute-plan.md entry pointed at line 415, which ffc190df4's
  checkpoint-exclusion caveat (added near line 221) shifted down by one
  line. The actual "validated downstream by gsd-tools uat
  classify-coverage" descriptive mention now sits at line 416. Updated
  the allowlist entry's line number to match.

- tests/task-command-router-resolve-content.test.cjs: the path-traversal
  test asserted the outside-project-scope diagnostic against the thrown
  ExitError's own .message. io.cts's error() (ADR-3889) writes its
  human-readable message to fd 2 via writeAllSync and then throws a bare
  `new ExitError(1)` with no message argument — by design, so the
  exception carries no duplicate text and the thrown ExitError's message
  defaults to "process exit 1" (cli-exit.cts's ExitError constructor).
  Root cause was the test, not the source: task-command-router.cjs's
  outside-project-scope rejection already calls error() correctly and the
  diagnostic text is genuinely emitted, just on fd 2, not on the
  exception. Fixed the test to capture fd-2 writes (mirroring
  tests/estimate-calibrate.test.cjs's runCalibrateExpectError and this
  same file's own captureStdout for fd 1) and assert against the captured
  stderr text instead of err.message. This was masked locally because a
  manual `node -e` sanity check that only inspects the caught exception's
  .message cannot see what the real node:test run actually failed on.

Emitted-Drift-Ack-Growth: execute-plan.md — adds the ADR-3646 task-content-resolution bullet and checkpoint-exclusion caveat to the per-task execute loop; a real behavioral prose addition, not incidental bloat.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#3970): backfill changeset PR number (pr:0 -> pr:4000)

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 13:17:04 -04:00

191 lines
10 KiB
JavaScript

'use strict';
/**
* Pinned baseline for scripts/lint-docs-guard-registration.cjs's
* `docs-guard-exempt` identity ratchet — mirrors
* scripts/lint-allow-test-rule-refs.cjs's `allow-test-rule` ratchet
* (scripts/lib/allowlist-ratchet.cjs, ADR-456's pattern).
*
* Every test file basename here is a KNOWN, already-reviewed
* `// docs-guard-exempt: <reason>` marker. Nothing caps how many files may
* carry the marker in the abstract, but ADDING a new one anywhere in
* tests/*.test.cjs fails `lint-docs-guard-registration` until this baseline
* is deliberately updated — so a new exemption always shows up as a
* reviewable diff here, instead of silently opting a file out of the
* docs-guard registration requirement with zero signal.
*
* A file listed here that no longer exists, or no longer carries a real
* `docs-guard-exempt:` marker in its first 20 lines, is reported STALE and
* must be pruned (ratchet-down, same as the sibling gate).
*
* Seeded 2026-08-23 (#3753 security follow-up FIX 2) from every real marker
* found via scripts/lint-docs-guard-registration.cjs's own `findExemption`
* scan of tests/*.test.cjs — derived, not retyped from memory.
*
* Extended 2026-08-23 (#3753 correctness follow-up): the reader-detection
* fixes (Defects A/B/C) surfaced 86 previously-undetected docs/ readers. 40
* were genuine docs-content guards (added to DOCS_GUARD_TESTS in
* scripts/docs-guard-registry.cjs); the 46 added here touch a docs/ path
* only incidentally (fixture data, external URL citations, comment-only
* mentions, metadata labels, or scan-exclusion lists) — see each file's own
* `docs-guard-exempt:` marker for its specific reason.
*/
const DOCS_GUARD_EXEMPT_BASELINE = [
'adr-parser.test.cjs',
'adr-parser.unit.test.cjs',
'agent-marker-documentation-guard.test.cjs',
'antigravity-upgrades.test.cjs',
'capability-cli.test.cjs',
'capability-validator-task-content-resolver.test.cjs',
'ci-docs-guard-registry.test.cjs',
'ci-test-scope.test.cjs',
'cline-install.test.cjs',
'code-review-depth.test.cjs',
'code-review-pipeline-regression.test.cjs',
'codebuddy-upgrades.test.cjs',
'commands.test.cjs',
'commit-docs-bypass.test.cjs',
'complexity-trigger.test.cjs',
'concurrency-safety.test.cjs',
'cursor-imperative-reference.test.cjs',
'declarative-reference-antigravity.test.cjs',
'declarative-reference-zcode.test.cjs',
'emitted-attribution.test.cjs',
'eslint-rules.test.cjs',
'estimate-calibrate.test.cjs',
'gen-context-index.test.cjs',
'gen-registry.test.cjs',
'gsd-agent-isolation-guard.test.cjs',
'hermes-dispatch-upgrade.test.cjs',
'install-minimal-hooks.test.cjs',
'install-runtime-artifacts.test.cjs',
'installer-migration-config-root-marker.test.cjs',
'installer-migration-pi-extension-ext.test.cjs',
'installer-migrations.test.cjs',
'kimi-upgrades.test.cjs',
'lint-allow-test-rule-refs.test.cjs',
'lint-docs-command-form.test.cjs',
'lint-docs-required.test.cjs',
'manifest-version-sync.test.cjs',
'milestone-archive.test.cjs',
'model-resolver.test.cjs',
'new-project-mvp-prompt.test.cjs',
'onboard-command.test.cjs',
'opencode-command-dir-plural.test.cjs',
'phase.test.cjs',
'pr-branch-planning-filter.test.cjs',
'precommit-alias-drift-hook.test.cjs',
'removed-but-needed-lint.test.cjs',
'repo-invariants.test.cjs',
'require-issue-link-policy.test.cjs',
'reviewer-manifest-body.test.cjs',
'run-tests-harness.test.cjs',
'runtime-name-policy.test.cjs',
'security-prompt-injection.security.test.cjs',
'shipped-reference-cites.test.cjs',
'state.test.cjs',
'worktree-safety.test.cjs',
];
/**
* Security follow-up FIX 3: a per-file fingerprint of every distinct
* `docs/...` path TOKEN referenced by each baselined file (derived via
* scripts/lint-docs-guard-registration.cjs's `extractDocsPathReferences`,
* never retyped from memory). A baselined file whose live fingerprint
* DIFFERS from the recorded one here fails the lint — the exemption's
* premise ("this file doesn't really guard shipped docs content") may no
* longer hold and a human must re-confirm it before the entry is updated.
* Keeping this a plain, sorted, diffable path list (not a hash) is
* deliberate: a reviewer can see exactly WHAT changed from the PR diff
* alone.
*
* Seeded 2026-08-23 alongside DOCS_GUARD_EXEMPT_BASELINE above, from the
* exact same scan.
*/
const DOCS_GUARD_EXEMPT_DOCS_PATHS = {
'adr-parser.test.cjs': ['docs/adr/0001.md', 'docs/adr/0002.md', 'docs/adr/0010.md', 'docs/adr/NNNN.md'],
'adr-parser.unit.test.cjs': ['docs/adr/0001.md', 'docs/adr/0099.md', 'docs/adr/NNNN.md'],
'agent-marker-documentation-guard.test.cjs': ['docs/reference', 'docs/reference/workflow-fragments.md'],
'antigravity-upgrades.test.cjs': ['docs/cli', 'docs/cli/gcli-migration', 'docs/cli/permissions'],
'capability-cli.test.cjs': ['docs/reference/gsd-capability-command.md'],
// #3970: cites docs/adr/3646-per-task-content-resolution-seam.md in an
// explanatory comment describing ADR-3646's Decision 3; the file never
// reads that (or any) docs/ file.
'capability-validator-task-content-resolver.test.cjs': ['docs/adr/3646-per-task-content-resolution-seam.md'],
'ci-docs-guard-registry.test.cjs': [
'docs/AGENTS.md', 'docs/COMMANDS.md', 'docs/INVENTORY.md', 'docs/a.md', 'docs/adr',
'docs/adr/0001-example.md', 'docs/adrenaline.md', 'docs/bar.md', 'docs/foo.md', 'docs/how-to/foo.md',
'docs/how-to/some-unrelated-guide.md', 'docs/how-to/x.md', 'docs/some-unrelated-file.md',
'docs/totally-unrelated.md',
],
'ci-test-scope.test.cjs': [
'docs/a.md', 'docs/adr', 'docs/adr/22-plan-drift-guard.md', 'docs/how-to/configure-model-profiles.md',
'docs/installer-migrations.md', 'docs/ja-JP', 'docs/ja-JP/USAGE.md', 'docs/usage.md', 'docs/x.md',
],
'cline-install.test.cjs': ['docs/guide.md'],
'code-review-depth.test.cjs': ['docs/src/auth/x.ts'],
'code-review-pipeline-regression.test.cjs': ['docs/DEVELOPMENT.md'],
'codebuddy-upgrades.test.cjs': ['docs/cli/sub-agents'],
'commands.test.cjs': ['docs/x.md'],
'commit-docs-bypass.test.cjs': [
'docs/40-design.md', 'docs/CONFIGURATION.md', 'docs/readme.md', 'docs/tracked-var-mentioning',
],
'complexity-trigger.test.cjs': ['docs/readme.md'],
// #3884: cites docs/CLI-TOOLS.md:736 in an explanatory comment describing
// the real `frontmatter get <file> [--field key]` CLI shape; the file
// never reads that (or any) docs/ file.
'concurrency-safety.test.cjs': ['docs/CLI-TOOLS.md'],
'cursor-imperative-reference.test.cjs': ['docs/sdk/typescript'],
'declarative-reference-antigravity.test.cjs': ['docs/cli/features'],
'declarative-reference-zcode.test.cjs': ['docs/reference/host-integration-capability-matrix.md'],
'emitted-attribution.test.cjs': ['docs/README.md', 'docs/tests', 'docs/tests/helpers/install-shared.cjs'],
'eslint-rules.test.cjs': ['docs/readme.md'],
'estimate-calibrate.test.cjs': [
'docs/adr', 'docs/adr/2629-phase-effort-estimation-calibration.md', 'docs/reference',
'docs/reference/planning-artifacts.md',
],
'gen-context-index.test.cjs': ['docs/CONTEXT-INDEX.json', 'docs/INVENTORY-MANIFEST.json'],
'gen-registry.test.cjs': ['docs/registries', 'docs/registries/reviewers.json'],
'gsd-agent-isolation-guard.test.cjs': ['docs/adr/1239-...md', 'docs/adr/1239-gsd-embeddable-orchestration-engine.md'],
'hermes-dispatch-upgrade.test.cjs': ['docs/guides/delegation-patterns.md'],
'install-minimal-hooks.test.cjs': ['docs/en/hooks', 'docs/en/users/features/hooks'],
'install-runtime-artifacts.test.cjs': ['docs/CONFIGURATION.md', 'docs/adr/58-...md', 'docs/adr/58-runtime-install-policy-module.md', 'docs/cli/slash-commands'],
'installer-migration-config-root-marker.test.cjs': ['docs/installer-migrations.md'],
'installer-migration-pi-extension-ext.test.cjs': ['docs/installer-migrations.md'],
'installer-migrations.test.cjs': ['docs/installer-migrations.md'],
'kimi-upgrades.test.cjs': ['docs/reference/host-integration-capability-matrix.md'],
'lint-allow-test-rule-refs.test.cjs': ['docs/readme.md'],
'lint-docs-command-form.test.cjs': ['docs/adr', 'docs/adr/999-example.md', 'docs/how-to/example.md'],
'lint-docs-required.test.cjs': [
'docs/COMMANDS.md', 'docs/USER-GUIDE.md', 'docs/adr', 'docs/adr/0001-foo.md', 'docs/adr/0099-new.md',
'docs/agents', 'docs/agents/triage-labels.md',
],
'manifest-version-sync.test.cjs': [],
// #3884: re-confirmed — the added docs/CLI-TOOLS.md:458 reference is the
// same class as the existing docs/TESTING-SUITES.md one (a placement-note
// / explanatory comment citing documented CLI behavior for context, never
// a read target); the exemption's premise still holds for both.
'milestone-archive.test.cjs': ['docs/CLI-TOOLS.md', 'docs/TESTING-SUITES.md'],
'model-resolver.test.cjs': ['docs/TESTING-SUITES.md'],
'new-project-mvp-prompt.test.cjs': ['docs/CONFIGURATION.md'],
'onboard-command.test.cjs': ['docs/adr/0001-runtime.md'],
'opencode-command-dir-plural.test.cjs': ['docs/commands'],
'phase.test.cjs': ['docs/adr/3524-...md', 'docs/adr/3524-cjs-sdk-hard-seam.md'],
'pr-branch-planning-filter.test.cjs': ['docs/readme.md'],
'precommit-alias-drift-hook.test.cjs': ['docs/adr/0174-...md', 'docs/adr/0174-retire-gsd-sdk-package-boundary.md'],
'removed-but-needed-lint.test.cjs': [
'docs/README.md', 'docs/getting-started.md', 'docs/gsd-new-workspace.md', 'docs/setup.md', 'docs/some-doc.md',
],
'repo-invariants.test.cjs': ['docs/FEATURES.md', 'docs/workflows/README'],
'require-issue-link-policy.test.cjs': ['docs/-prefixed', 'docs/CONFIGURATION.md', 'docs/a.md', 'docs/b.md', 'docs/guide.md'],
'reviewer-manifest-body.test.cjs': ['docs/how-to/ship-a-reviewer-lane.md'],
'run-tests-harness.test.cjs': ['docs/TESTING-SUITES.md'],
'runtime-name-policy.test.cjs': ['docs/customize/skills'],
'security-prompt-injection.security.test.cjs': ['docs/notes.md'],
'shipped-reference-cites.test.cjs': [],
'state.test.cjs': ['docs/CONFIGURATION.md'],
'worktree-safety.test.cjs': ['docs/SUMMARY.md'],
};
module.exports = { DOCS_GUARD_EXEMPT_BASELINE, DOCS_GUARD_EXEMPT_DOCS_PATHS };