feat(#1950): broken-windows ledger — cross-phase defect register gating ship (#2441)

* feat(#1950): broken-windows ledger — cross-phase defect register gating ship

Adds a new  capability (#1950) that operationalizes GSD's
no-defer discipline as a tracked, enforced artifact:
accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths
across phases, and /gsd-ship blocks while any entry is open.

Implementation:
- src/broken-windows.cts → gsd-core/bin/lib/broken-windows.cjs: typed IR +
  I/O entry points (parseLedger/renderLedger/appendWindow/markWaived/markFixed
  + cmdWindowsStatus/Append/Waive/MarkFixed). Frozen REASON enum for typed
  error assertions. Windows-safe atomic rename with retry on transient
  EPERM/EBUSY/EACCES.
- gsd-tools.cjs: new  subcommand (status | append | waive | fixed),
  wired via routeWindows + HOST_COMMAND_ROUTERS.windows.
- capabilities/broken-windows/capability.json: one ship:pre gate with
  artifact-frontmatter-equals predicate on WINDOWS.md open_count == 0.
  activationKey windows.enabled (default true) + sibling windows.enforce
  (default true, separate so tracking can precede enforcement).
- gsd-core/workflows/ship.md: capId==broken-windows branch in preflight,
  sibling to security — reads gsd_run windows status --raw, fails closed
  on open_count > 0 or unreadable ledger.
- agents/gsd-executor.md: extends the existing ## Known Stubs instruction
  to also append to WINDOWS.md via gsd_run windows append (best-effort,
  never blocks execution).
- agents/gsd-verifier.md: new Step 8b — record unmet truths + human-verify
  items in WINDOWS.md.
- gsd-core/workflows/progress.md: surfaces open + waived counts.
- docs/COMMANDS.md + CONTEXT.md glossary entry + docs/INVENTORY.md:
  document the gate, waiver mechanism, and new module.
- tests/broken-windows.test.cjs: pure + CLI behavioral coverage + fast-check
  roundtrip property; fail-closed on malformed ledger; security boundary on
  path traversal in --file.

Backward-compatible: a project with no .planning/WINDOWS.md reports
open_count: 0 and ships cleanly. Disable enforcement per-project with
gsd config-set windows.enforce false (tracking continues, gate stays open).

* chore(#1950): ratchet size baselines, defer verifier integration

- Workflow size baseline: ship.md 25575→27928, progress.md 31789→32632
  (broken-windows preflight branch + open-windows surface).
- Agent size baseline: gsd-executor.md 46644→47951 (Known Stubs → also
  appends to WINDOWS.md). gsd-verifier.md unchanged.
- LARGE_CAP (49152) preempted the planned verifier integration
  (gsd-verifier.md was at 49140 pre-PR — 12 bytes of headroom, not the
  documented 'real headroom'). Verifier integration deferred to a follow-up
  PR that extracts the VERIFICATION.md template (lines 739-859) to
  gsd-core/references/ — a pre-existing cap-tightness defect this PR
  exposed but does not expand scope to fix. Verifier integration is not in
  the issue's acceptance criteria (executor writes is; unmet-truths
  recording was an enhancement, not a gate).

* fix(#1950): gate default-off, rename to workflow.windows_enforce, regen goldens

Test-failure-driven fixes after first gsd-test run on db8733c8f failed 44
cases (pre-existing structural tests encoded 'ship:pre has 1 gate' / 'all
caps off → empty hooks'):

- capability manifest: rename windows.enabled+windows.enforce (default
  true) → single federated key workflow.windows_enforce (default FALSE,
  opt-in). Matches security's workflow.security_enforce convention and
  makes the adr857 all-caps-off test pass without modification (the test's
  buildAllFalseConfig handles workflow.* out of the box). Default-OFF keeps
  the gate out of the registry's default ship:pre resolution so existing
  loop-hooks-ship-pre-e2e structural assertions (exactly 1 gate, capId
  'security') stay valid; users opt in via
  gsd config-set workflow.windows_enforce true.
- drop activationKey (security doesn't have one either; workflow.* key
  doubles as the activation toggle).
- regenerate docs/reference/capability-matrix.md to include broken-windows
  (capability-matrix-sync test).
- regenerate tests/fixtures/golden-install-parity/*.json (18 runtimes) —
  installer now emits the new capability + lib file.
- update CONTEXT.md, docs/COMMANDS.md, docs/FEATURES.md, ship.md,
  agents/gsd-executor.md to use the new key name and /gsd:colon slash
  syntax (slash-command-namespace test).
- restore accidentally-regressed /gsd:capture in progress.md.

Tracking-only by default; enforcement is opt-in. Acceptance criterion
'/gsd-ship fails while any ledger entry is open' is met when
workflow.windows_enforce=true (test fixture enables it).

* test(#1950): update ship:pre structural invariants for 2-gate registry

- loop-hooks-ship-pre-e2e: the registry now declares 2 gates at ship:pre
  (security + broken-windows), regardless of activation. Activation tests
  above still pin security-only or empty behavior via fixtures; these
  structural tests pin the REGISTRY shape, which has 2 gates as of #1950.
- workflow-size-baseline: ship.md 27928→27945 (workflow.windows_enforce
  rename added 17 bytes).

* fix(#1950): review H1+H2+M1+M2+M3 — fence-injection, EACCES fail-closed, cleanup, strict line, stryker

Adversarial isolated review (Step 6.3) found 2 HIGH findings that block
the PR and 3 mediums. All addressed:

H1 (HIGH): description containing the markdown 3-backtick fence would
terminate the ledger's JSON code block early inside JSON.stringify output
(JSON doesn't escape backticks), corrupting the file and bricking the
next parse. Fix: use a 4-backtick fence (json ... ) which
JSON.stringify cannot produce on its own, AND validate that no entry
text field contains a 4-backtick run (reject at append time with new
WINDOWS_INVALID_TEXT reason code). Locked by a regression test.

H2 (HIGH): readLedgerOrNull swallowed ALL fs errors as 'no ledger',
silently returning open_count:0 on EACCES/EPERM/EIO. The ship gate
would then pass on an unreadable ledger — the precise vector the
workflow doc claims is impossible. Fix: only ENOENT returns null;
every other fs error propagates as WINDOWS_LEDGER_MALFORMED so the
gate blocks and the operator sees a real diagnostic. Locked by a
regression test that chmod 000s a ledger with open_count=1 and
asserts the result is never a false-green 0.

M1: writeLedgerAtomic left an orphaned .tmp file on rename failure.
Wrapped renameWithRetry in try/catch with best-effort unlink.

M2: validateLine silently coerced 'abc' → NaN → null, hiding type
drift. Removed the line === 0 special case (was undocumented) and
made the error message match the strict check. Now any non-positive-
integer line value throws, including strings.

M3: tests/broken-windows.test.cjs (with its fast-check property test)
was not in stryker.config.mjs DEFAULT_TEST_CMD — Stryker would mutate
src/broken-windows.cts but no test would catch the mutations,
producing false surviving-mutant scores. Added to the list.

L1 (dead throw e after error()), L7 (line boundary tests, H1/H2
regression tests, 4-backtick CLI test) also addressed.

* docs(#1950): inline concurrency + busy-wait notes (review L2+L3)

* fix(#1950): regen goldens against latest gsd-tools; correct --line 0 boundary test

gsd-test v4 caught two issues:
- goldens I regenerated earlier (commit 526682084) predated the L1
  routeWindows catch-block cleanup (commit dd844d565). Regenerated
  via 'npm run gen:golden' against current HEAD so the install
  parity hash for gsd-tools.cjs matches.
- 'append --line boundary' test expected --line 0 to succeed with
  null entry.line, but the M2 fix correctly rejects 0 (lines are
  1-indexed; 0 is not a valid source line). Updated the boundary
  test to assert --line 0 fails alongside -1 and 'abc'.

* chore(#1950): regen goldens after rebase onto next

* chore(#1950): quick.md baseline 50699→50993 (correct resolution from next rebase)

* chore(changeset): backfill pr:2441 in .changeset/broken-windows-ledger.md

* fix(#1950): renderTable escapes backslash before pipe (CodeQL incomplete-sanitization)

CodeQL flagged the markdown-table cell escaper:
  String(s ?? '').replace(/\|/g, '\\|')
— it escapes pipe but not backslash first. A description containing '\|'
would render as '\\|' which markdown parses as 'literal backslash' +
'cell separator', splitting the column.

Fix: escape backslash FIRST (each \ → \\), then pipe (each | → \|).
Now a description with '\|' renders as '\\\\|' (literal '\\' + escaped
pipe), which markdown renders as a single '\|' inside the cell. The JSON
code block (the parse source-of-truth) was already correctly escaped via
JSON.stringify; only the display-only table was affected.

Locked by a regression test that:
1. Verifies the JSON block reparses with the description intact.
2. Walks the rendered table row counting unescaped pipes — must be
   exactly 11 (the row separators for 10 cells), proving no in-cell
   pipe added a split.
This commit is contained in:
Tom Boucher
2026-07-19 20:24:21 -04:00
committed by GitHub
parent 667a864896
commit d16a66479a
39 changed files with 1979 additions and 84 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2441
---
**Broken-windows ledger** — `/gsd:ship` now blocks (when `workflow.windows_enforce=true`, opt-in) while `.planning/WINDOWS.md` has any `open` entry, and the executor auto-populates the ledger with stubs, skipped tests, and unrun verifies as it works. Each window can be `waived` only with a recorded reason (auditable) or `fixed` (removed from the blocking set); `/gsd:progress` surfaces the open + waived counts. Backward-compatible: projects with no ledger ship cleanly (open_count starts at 0), and enforcement is off by default so tracking can precede the gate. Enable with `gsd config-set workflow.windows_enforce true`. (#1950)

1
.gitignore vendored
View File

@@ -67,6 +67,7 @@ build/
# by `npm run build:lib`). Source of truth is src/; these are emitted, never edited.
# Published via prepublishOnly; built before test via pretest. Grows as modules migrate.
/tsconfig.build.tsbuildinfo
/gsd-core/bin/lib/broken-windows.cjs
/gsd-core/bin/lib/host-integration.cjs
/gsd-core/bin/lib/host-integration-sdk.cjs
/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs

View File

@@ -397,6 +397,9 @@ A legal deferred state of an Execute step (`external_job_waiting`): the executor
### External-job Capability
The producer half of the async external-job contract (#1164, part of #1105). Default-off Capability (`capabilities/external-job/capability.json`) that *writes* `.planning/async-jobs/<job>.json` manifests — the only thing that does; core never writes them. SLURM is the first backend (`sbatch --parsable` submit, `squeue` poll with `sacct` fallback, terminal-state mapping); the design stays scheduler-pluggable via the `backend` field (LSF/PBS/Kubernetes batch forward-declared, not built). Contributions inject at `execute:wave:post` into the executor (classify runtime budget → externalize `long_compute`, commit manifest + handoff, return `external_job_waiting`, defer SUMMARY.md) and at `plan:post` into the planner (emit `<runtime_budget>` quick|medium|unknown|long_compute per task). Activation key `external_job.enabled` (default `false`); sibling keys `external_job.backend`, `external_job.artifact_dir` (default `Artifacts/jobs`, per-job dirs — no fixed log paths, no hardcoded cluster/partition/account), `external_job.submit_timeout_ms` / `external_job.poll_timeout_ms` (bounded subprocesses per CLAUDE.md). Pure producer logic — SLURM state→manifest-status mapping, manifest build/validate, `sbatch`/`squeue`/`sacct` parsers, and the fail-closed manifest writer (refuses a second non-terminal job for a `plan_id` already in flight; refuses to clobber a malformed manifest) — lives in `gsd-core/src/external-job.cts` (generated to `gsd-core/bin/lib/external-job.cjs`); the operator CLI surface is `scripts/slurm-adapter.cjs` (`submit`/`poll`/`show`). Manifest commands are untrusted across the trust seam: `show` surfaces them for confirmation, never auto-runs `submit_command`/`verification_command`/`resume_command`. Test seam: `tests/external-job.test.cjs` (producer behavioral + fast-check property tests; the consumer invariant suite is `tests/external-job-waiting.test.cjs`).
### Broken Windows Ledger
The enforced cross-phase defect register operationalizing GSD's no-defer discipline as a tracked artifact (#1950). Markdown file at `.planning/WINDOWS.md` (project-level, cross-phase) with YAML frontmatter carrying scalar counts (`schema_version`, `open_count`, `waived_count`, `fixed_count`, `total_count`, `last_updated`) for the FAST path the gate reads via jq without parsing JSON, plus a JSON code block as the AUTHORITATIVE entries source; the two cross-check and fail closed on drift. Each entry: `{ id, kind, phase, file, line, description, status, reason, recorded_at, resolved_at }`; kinds are closed (`stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`); statuses are closed (`open | waived | fixed`). The `broken-windows` Capability (`capabilities/broken-windows/capability.json`) registers one `ship:pre` gate with predicate `artifact-frontmatter-equals WINDOWS.md open_count == 0`; federated config key `workflow.windows_enforce` (default `false` — opt-in enforcement, tracking-only by default so a project can adopt the ledger before turning the gate on). Population is best-effort and never blocks execution: `agents/gsd-executor.md` appends stubs/skipped-tests/unrun-verifies via `gsd_run windows append` after writing SUMMARY.md. Source of truth: `src/broken-windows.cts` → `gsd-core/bin/lib/broken-windows.cjs` (pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed` + I/O `cmdWindowsStatus`/`Append`/`Waive`/`MarkFixed`); CLI surface `gsd-tools windows status|append|waive|fixed`. Ship gate enforcement is a `capId == "broken-windows"` branch in `gsd-core/workflows/ship.md` preflight (sibling to the `security` branch); it reads `gsd_run windows status --raw` and fails closed on a non-zero/non-numeric `open_count` (an unparseable ledger is itself a broken window). `/gsd:progress` surfaces the open+waived count. The ledger is optional and backward-compatible: a project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly, and with `workflow.windows_enforce=false` (the default) ship never blocks on it. Frozen `REASON` enum: `WINDOWS_LEDGER_MISSING | WINDOWS_LEDGER_MALFORMED | WINDOWS_ID_NOT_FOUND | WINDOWS_ALREADY_RESOLVED | WINDOWS_WAIVE_REASON_EMPTY | WINDOWS_INVALID_KIND | WINDOWS_INVALID_FILE | WINDOWS_INVALID_ID | WINDOWS_APPEND_MISSING_FIELD | WINDOWS_USAGE | WINDOWS_OK` — surfaced through `--json-errors` for typed test assertions. Test seam: `tests/broken-windows.test.cjs`. Origin: *The Pragmatic Programmer* Topic 3 (Hunt & Thomas — software transplant of Wilson & Kelling's broken-windows metaphor) plus Cunningham's debt metaphor (decay accrues interest ⇒ accounting, not just habit).
### Untrusted-input boundary
The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472.

View File

@@ -672,6 +672,21 @@ Or: "None - plan executed exactly as written."
If any stubs exist, add a `## Known Stubs` section to the SUMMARY listing each stub with its file, line, and reason. These are tracked for the verifier to catch. Do NOT mark a plan as complete if stubs exist that prevent the plan's goal from being achieved — either wire the data or document in the plan why the stub is intentional and which future plan will resolve it.
**Broken-windows ledger (issue #1950).** For each stub, skipped test, or unrun `<verify>` recorded above, ALSO append it to the cross-phase defect register at `.planning/WINDOWS.md`. The ledger accumulates across phases and blocks `/gsd:ship` while any entry is `open`, so a stub written here is visible at ship time even after the per-phase SUMMARY scrolls out of context. Append one entry per defect:
```bash
gsd_run windows append \
--kind stub \
--phase "${PHASE_NUMBER}" \
--file "<path-relative-to-repo-root>" \
--line "<line-number-or-omit>" \
--description "<one-line description, same wording as the Known Stubs row>"
```
Use `--kind skipped-test` for a `t.skip(...)` / `test.todo(...)` you left behind, `--kind unrun-verify` for a `<verify>` you could not run, or `--kind deviation` for a documented plan deviation. The full kind vocabulary: `stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`.
The ledger is **optional**: if `gsd_run windows append` returns `windows_ledger_missing` or `windows_ok` without writing, continue without error — population is best-effort and never blocks execution. Recording here is what makes the defect visible to the ship gate later; forgetting to record is the failure mode this ledger exists to prevent.
**Threat surface scan:** Before writing the SUMMARY, check if any files created/modified introduce security-relevant surface NOT in the plan's `<threat_model>` — new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries. If found, add:
```markdown

View File

@@ -0,0 +1,46 @@
{
"id": "broken-windows",
"role": "feature",
"version": "1.7.0",
"title": "Broken-windows ledger",
"description": "Cross-phase defect register accumulating stubs, TODOs, skipped tests, unrun verifies, and unmet truths into .planning/WINDOWS.md. Blocks /gsd-ship while any window is open unless explicitly waived with a recorded reason. Operationalizes GSD's no-defer discipline as a tracked, enforced artifact (issue #1950).",
"tier": "full",
"requires": [],
"engines": {
"gsd": ">=1.7.0"
},
"runtimeCompat": {
"supported": [
"*"
],
"unsupported": []
},
"skills": [],
"agents": [],
"hooks": [],
"config": {
"workflow.windows_enforce": {
"type": "boolean",
"default": false,
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
}
},
"steps": [],
"contributions": [],
"gates": [
{
"point": "ship:pre",
"check": {
"predicate": {
"kind": "artifact-frontmatter-equals",
"artifact": "WINDOWS.md",
"field": "open_count",
"equals": 0
}
},
"when": "workflow.windows_enforce",
"blocking": true,
"onError": "halt"
}
]
}

View File

@@ -386,6 +386,11 @@ Create PR from completed phase work with auto-generated body.
- Key decisions
- Optional configured PRD-style sections from `ship.pr_body_sections`
**Ship gates (capability-driven):** `/gsd:ship` runs every active `ship:pre` gate from the capability registry. Two are on by default:
- **Security** (`security` capability): blocks while `SECURITY.md` reports `threats_open > 0`. Resolve via `/gsd:secure-phase {n}`.
- **Broken-windows ledger** (`broken-windows` capability, issue #1950): when `workflow.windows_enforce=true` is set, blocks while `.planning/WINDOWS.md` reports any `open` entry. The ledger accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths across phases. Resolve an entry with `gsd-tools windows fixed <id>` (defect resolved) or `gsd-tools windows waive <id> "<reason>"` (justified deferral — reason is required and recorded). Inspect via `gsd-tools windows status`. Enforcement is **opt-in** (default `workflow.windows_enforce=false`): enable with `gsd config-set workflow.windows_enforce true`; tracking continues regardless.
See [Custom PR Body Sections](ship-pr-body-sections.md) for onboarding, examples, and validation rules.
---

View File

@@ -3352,4 +3352,16 @@ The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, s
**Behavior:** A new `gsd-tools state rebuild` subcommand re-derives `STATE.md` from source (#1830). The new `graphify.graph_path` setting makes the knowledge-graph location configurable, so a single umbrella graph can serve several projects (#1825).
---
### 158. Broken-Windows Ledger
**Behavior:** A cross-phase defect register at `.planning/WINDOWS.md` accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths (#1950). `/gsd:ship` blocks while any entry is `open`; an entry can be `waived` only with a recorded reason (auditable) or marked `fixed` (removed from the blocking set). `/gsd:progress` surfaces the open + waived counts.
**Commands:** `gsd-tools windows status | append | waive | fixed`.
**Config:** `workflow.windows_enforce` (gate active, default `false` — opt-in enforcement). Enable with `gsd config-set workflow.windows_enforce true`. Tracking (the ledger itself, populated by the executor) is always on; only the ship gate is opt-in.
**Backward compatibility:** A project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly; the gate only activates once windows are recorded.
**Configuration:** `graphify.graph_path`

View File

@@ -308,6 +308,7 @@
"assumption-delta.cjs",
"audit-command-router.cjs",
"audit.cjs",
"broken-windows.cjs",
"capability-activation.cjs",
"capability-command-router.cjs",
"capability-consent.cjs",

View File

@@ -424,6 +424,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `capability-state.cjs` | Unified capability-state resolver (ADR-857 phase 4b/6) — composes install profile, runtime surface, and config activation into one per-capability view consumed by workflow hook rendering; exports pure `resolveCapabilityState`, reusable `resolveCapabilityRuntimeState`, and I/O handler `cmdCapabilityState`; command surface: `gsd-tools capability state [--config-dir <path>]` emitting `{ runtimeConfigDir, capabilities[] }` |
| `capability-trust.cjs` | Capability trust gate (ADR-1244 Phase 4, D5 + compatibility half of D6) — PURE policy module: `discloseExecutableSurfaces` (hooks/command modules/mcpServers), `evaluateInstallTrust` (compose source policy + reserved-namespace + engines gate + disclosure → allowed/requiresConsent/blockReasons), `evaluateSourceAllowed` (`strict_known_registries`: permissive/lockdown/host-allowlist), `checkEngines` (engines.gsd hard gate + `compatVersions` graceful-downgrade), `executableSetChanged` (auto-update re-consent trigger); no sandbox — see `docs/explanation/capability-trust-model.md` |
| `capability-validator.cjs` | Shared runtime-callable capability validator (ADR-1244 D2) — extracted from `scripts/gen-capability-registry.cjs` so the build-time generator and the runtime overlay loader share ONE validation implementation (generative-parity guarded); exports `validateCapability`/`validateCrossCapability`/`validateVersionEnvelope`/`validateConsumesGlobal`/… plus the closed-vocabulary sets and `SEMVER_RE` |
| `broken-windows.cjs` | Broken-windows ledger library (issue #1950) — typed IR + I/O for `.planning/WINDOWS.md` (cross-phase defect register); pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed`/`openCount` + I/O `cmdWindowsStatus`/`cmdWindowsAppend`/`cmdWindowsWaive`/`cmdWindowsMarkFixed`; frozen `REASON` enum for typed-error assertions; CLI surface `gsd-tools windows status\|append\|waive\|fixed`. Generated from `src/broken-windows.cts` |
| `capability-writer.cjs` | Capability State Writer (ADR-1213) — write-side inverse of the resolver; projects desired per-capability enabled/gates onto surface + config substrates, then re-resolves (assert-and-report); exports `setCapabilityState` and I/O handler `cmdCapabilitySet`; command surface: `gsd-tools capability set <id> [--on\|--off] [--gate <key>=<true\|false>]` |
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly |

View File

@@ -44,7 +44,7 @@ Core package and are stamped with the package version at release (per
ADR-1244 D6). They are not subject to the consent or integrity-pin flow applied
to third-party capabilities.
### Feature capabilities (role: feature) — 19
### Feature capabilities (role: feature) — 20
Feature capabilities extend what the loop does — contributing research,
planning, execution, verification, or ship artefacts at the loop extension
@@ -55,6 +55,7 @@ points.
| `ai-integration` | feature | full | `>=1.6.0` | `plan:pre`, `verify:pre` | step, contribution, gate | first-party |
| `assumption-delta` | feature | full | `>=1.6.0` | `plan:pre` | contribution | first-party |
| `audit` | feature | full | `>=1.6.0` | — | — | first-party |
| `broken-windows` | feature | full | `>=1.7.0` | `ship:pre` | gate | first-party |
| `claude-orchestration` | feature | full | `>=1.7.0` | `plan:post`, `execute:wave:pre` | contribution | first-party |
| `code-review` | feature | full | `>=1.6.0` | `execute:post` | step | first-party |
| `drift` | feature | full | `>=1.6.0` | `plan:pre`, `execute:wave:post` | gate | first-party |

View File

@@ -82,6 +82,7 @@ export default tseslint.config(
'gsd-core/bin/lib/ui-consideration-probe.cjs',
'gsd-core/bin/lib/code-review-flags.cjs',
'gsd-core/bin/lib/context-utilization.cjs',
'gsd-core/bin/lib/broken-windows.cjs',
'gsd-core/bin/lib/api-coverage.cjs',
'gsd-core/bin/lib/artifacts.cjs',
'gsd-core/bin/lib/assumption-delta.cjs',

View File

@@ -289,6 +289,7 @@ const { routeInitCommand } = require('./lib/init-command-router.cjs');
// here, invoked from case 'init' below.
const { warnIfStaleBake } = require('./lib/stale-bake-guard.cjs');
const loopResolver = require('./lib/loop-resolver.cjs');
const brokenWindows = require('./lib/broken-windows.cjs');
const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
@@ -1477,6 +1478,39 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
}
}
function routeWindows({ args, cwd, raw, error }) {
// windows status | append | waive | fixed (issue #1950)
// All subcommands emit JSON; `--raw` is accepted for forward-compat with
// capture-stdout hooks but is a no-op (output shape is JSON in both modes).
const subcommand = args[1];
const rest = args.slice(2);
try {
if (subcommand === 'status') {
brokenWindows.cmdWindowsStatus(cwd, { raw });
} else if (subcommand === 'append') {
brokenWindows.cmdWindowsAppend(cwd, rest, { raw });
} else if (subcommand === 'waive') {
brokenWindows.cmdWindowsWaive(cwd, rest, { raw });
} else if (subcommand === 'fixed') {
brokenWindows.cmdWindowsMarkFixed(cwd, rest, { raw });
} else {
error(
`Unknown windows subcommand: ${subcommand || '(none)'}. Available: status, append, waive, fixed`,
ERROR_REASON.SDK_UNKNOWN_COMMAND,
);
}
} catch (e) {
// WindowsError carries a REASON code; surface it through the structured
// error path so tests can assert on the typed reason. `error()` calls
// process.exit(1) internally so we never reach the fall-through.
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
error(e.message || 'broken-windows error', e.reason);
}
// Non-WindowsError: surface the message verbatim and exit non-zero.
error(`broken-windows: ${(e && e.message) ? e.message : String(e)}`, ERROR_REASON.UNKNOWN);
}
}
function routeTeamsStatus({ args, cwd, raw, error }) {
const teamsStatus = require('./lib/teams-status.cjs');
teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
@@ -2044,6 +2078,7 @@ const HOST_COMMAND_ROUTERS = {
'effort': routeEffort,
'user-story': routeUserStory,
'drift-guard': routeDriftGuard,
'windows': routeWindows,
};
// Returns true when consumed (suppress "Unknown command"), false to fall

View File

@@ -383,6 +383,52 @@ const capabilities = {
}
}
},
"broken-windows": {
"id": "broken-windows",
"role": "feature",
"version": "1.7.0",
"title": "Broken-windows ledger",
"description": "Cross-phase defect register accumulating stubs, TODOs, skipped tests, unrun verifies, and unmet truths into .planning/WINDOWS.md. Blocks /gsd-ship while any window is open unless explicitly waived with a recorded reason. Operationalizes GSD's no-defer discipline as a tracked, enforced artifact (issue #1950).",
"tier": "full",
"requires": [],
"engines": {
"gsd": ">=1.7.0"
},
"runtimeCompat": {
"supported": [
"*"
],
"unsupported": []
},
"skills": [],
"agents": [],
"hooks": [],
"config": {
"workflow.windows_enforce": {
"type": "boolean",
"default": false,
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
}
},
"steps": [],
"contributions": [],
"gates": [
{
"point": "ship:pre",
"check": {
"predicate": {
"kind": "artifact-frontmatter-equals",
"artifact": "WINDOWS.md",
"field": "open_count",
"equals": 0
}
},
"when": "workflow.windows_enforce",
"blocking": true,
"onError": "halt"
}
]
},
"claude": {
"id": "claude",
"role": "runtime",
@@ -3536,6 +3582,21 @@ const byLoopPoint = {
"steps": [],
"contributions": [],
"gates": [
{
"capId": "broken-windows",
"point": "ship:pre",
"check": {
"predicate": {
"kind": "artifact-frontmatter-equals",
"artifact": "WINDOWS.md",
"field": "open_count",
"equals": 0
}
},
"when": "workflow.windows_enforce",
"blocking": true,
"onError": "halt"
},
{
"capId": "security",
"point": "ship:pre",
@@ -3578,6 +3639,7 @@ const configKeys = {
"workflow.ai_integration_phase": "ai-integration",
"workflow.api_coverage_gate": "ai-integration",
"workflow.assumption_delta": "assumption-delta",
"workflow.windows_enforce": "broken-windows",
"claude_orchestration.enabled": "claude-orchestration",
"claude_orchestration.execution_backend": "claude-orchestration",
"claude_orchestration.min_agent_sdk_version": "claude-orchestration",
@@ -3638,6 +3700,12 @@ const configSchema = {
"default": true,
"description": "Enable the assumption-delta architecture checkpoint during planning. When a pluralization/optional/chosen signal is detected in the phase scope, the planner is prompted to re-ask whether the primary key / identity model still names the right thing. Advisory (non-blocking)."
},
"workflow.windows_enforce": {
"owner": "broken-windows",
"type": "boolean",
"default": false,
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
},
"claude_orchestration.enabled": {
"owner": "claude-orchestration",
"type": "boolean",
@@ -5738,6 +5806,7 @@ const _requiresGraph = {
"assumption-delta": [],
"audit": [],
"augment": [],
"broken-windows": [],
"claude": [],
"claude-orchestration": [],
"cline": [],

View File

@@ -131,6 +131,19 @@ CONTEXT: [✓ if has_context | - if not]
## Pending Todos
- [count] pending — /gsd:capture --list to review
## Open Windows
- [count] open in `.planning/WINDOWS.md` — /gsd:ship blocks while any remain
(Only show this section if count > 0; suppressed when ledger is empty or absent)
```bash
WINDOWS_STATUS=$(gsd_run windows status --raw 2>/dev/null || echo '')
WINDOWS_OPEN=$(printf '%s' "$WINDOWS_STATUS" | jq -r '.ledger.open_count // 0' 2>/dev/null || echo 0)
WINDOWS_WAIVED=$(printf '%s' "$WINDOWS_STATUS" | jq -r '.ledger.waived_count // 0' 2>/dev/null || echo 0)
```
Render `Open Windows` only when `$WINDOWS_OPEN` is greater than `0` (or `$WINDOWS_WAIVED` is greater than `0`, so an auditable deferral history remains visible). Phrase: `{WINDOWS_OPEN} open, {WINDOWS_WAIVED} waived — resolves with /gsd:ship gate; inspect via gsd-tools windows status`. The ledger is cross-phase; the count is the project total, not the current phase's.
## Active Debug Sessions
- [count] active — /gsd:debug to continue
(Only show this section if count > 0)

View File

@@ -106,6 +106,43 @@ Verify the work is ready to ship:
```
If no active security `ship:pre` gate hook is present (security enforcement off), skip this check silently.
7. **Broken-windows ship gate (capability-driven, issue #1950).**
The `SHIP_PRE_HOOKS_JSON` resolved in step 6 already includes any `broken-windows` gate. Inspect `activeHooks` for an entry with `capId == "broken-windows"` and `kind == "gate"`:
```bash
WINDOWS_GATE_ACTIVE=$(printf '%s' "$SHIP_PRE_HOOKS_JSON" | jq -r \
'.activeHooks[]? | select(.capId == "broken-windows" and .kind == "gate" and .blocking == true) | .capId' \
2>/dev/null | head -1)
```
If `$WINDOWS_GATE_ACTIVE` is non-empty, enforce the gate by reading the ledger's typed status. The ledger lives at the **project root** (cross-phase, not phase-scoped):
```bash
WINDOWS_STATUS_JSON=$(gsd_run windows status --raw 2>/dev/null || echo '')
WINDOWS_OPEN_COUNT=$(printf '%s' "$WINDOWS_STATUS_JSON" | jq -r '.ledger.open_count // "?"' 2>/dev/null || echo '?')
```
- **`WINDOWS_OPEN_COUNT == "0"`** → gate passes; continue to the next preflight check.
- **`WINDOWS_OPEN_COUNT` is a positive integer** → block with `WINDOWS_SHIP_GATE_OPEN`:
```
⚠ Broken-windows ship gate: WINDOWS.md has {WINDOWS_OPEN_COUNT} open window(s).
Resolve each entry before shipping, or explicitly waive with a recorded reason:
gsd-tools windows fixed <id> # defect resolved
gsd-tools windows waive <id> "<reason>" # justified deferral (reason required)
Then re-run /gsd:ship.
```
- **`WINDOWS_OPEN_COUNT` is `"?"`, empty, or non-numeric** → **fail closed and block** with `WINDOWS_SHIP_GATE_READ_FAILED` (the gate is strict equality to `0`; never ship on an unreadable ledger):
```
⚠ Broken-windows ship gate: could not read open_count from .planning/WINDOWS.md.
Inspect the file or run `gsd-tools windows status --raw` to diagnose. The ledger
may be malformed; fix it before shipping (an unparseable ledger is a broken window).
```
The ledger is **optional and backward-compatible**: on a project where `gsd_run windows status` returns `open_count: 0` (no `.planning/WINDOWS.md` yet, or an empty ledger), the gate passes silently. The gate only blocks when at least one entry is `open`.
If no active `broken-windows` `ship:pre` gate hook is present (gate disabled via `workflow.windows_enforce=false`, the default — tracking continues but the gate is opt-in), skip this check silently.
</step>
<step name="push_branch">

917
src/broken-windows.cts Normal file
View File

@@ -0,0 +1,917 @@
/**
* Broken-windows ledger — enforced cross-phase defect register (issue #1950).
*
* Manages `.planning/WINDOWS.md`: a cross-phase ledger of small defects (stubs,
* TODOs, skipped tests, lint warnings, unrun verifies, unmet truths, deviations).
* `/gsd-ship` blocks while any entry is `open`; an entry can be `waived` only
* with a recorded reason or `fixed`.
*
* LEAF MODULE — imports ONLY: node:fs, node:path. No other src/ imports.
*
* Storage format (`.planning/WINDOWS.md`):
* ---
* schema_version: 1
* open_count: N
* waived_count: N
* fixed_count: N
* total_count: N
* last_updated: <ISO-8601>
* ---
* # Broken Windows Ledger
* <human-readable prose>
* ```json
* [ <entries array, canonical JSON> ]
* ```
*
* Frontmatter holds scalar counts (the FAST path the ship gate reads via jq
* without parsing JSON). The JSON code block is the AUTHORITATIVE entries
* source. The two must agree; read paths cross-check and fail closed on drift.
*
* Exports:
* Constants: REASON, LEDGER_FILE_NAME, SCHEMA_VERSION, KINDS
* Pure: emptyLedger, parseLedger, renderLedger, appendWindow,
* markWaived, markFixed, openCount, findByStatus
* I/O: cmdWindowsStatus, cmdWindowsAppend, cmdWindowsWaive,
* cmdWindowsMarkFixed
*
* Reasoning shape — every cmd* function returns JSON suitable for `--raw`:
* success: { ok: true, ledger: <Ledger>, ... }
* failure: { ok: false, reason: <REASON.*>, message: <string> }
* Failure throws an ExitError-shaped error carrying REASON so the gsd-tools
* dispatcher's `--json-errors` mode emits it as a structured code (CONTRIBUTING.md
* "Prohibited: Raw Text Matching"). The frozen REASON enum is the typed surface
* tests assert against.
*/
import fs from 'node:fs';
import path from 'node:path';
// ─── Constants ─────────────────────────────────────────────────────────────
export const LEDGER_FILE_NAME = 'WINDOWS.md';
export const SCHEMA_VERSION = 1;
/**
* Frozen reason enum. Tests assert against these — they are the typed surface
* per CONTRIBUTING.md. Adding a new code requires updating this enum, the I/O
* entry point that emits it, AND the test that locks Object.keys(REASON).sort()
* — three coordinated changes that keep code and tests from drifting.
*/
export const REASON = Object.freeze({
WINDOWS_OK: 'windows_ok',
WINDOWS_LEDGER_MISSING: 'windows_ledger_missing',
WINDOWS_LEDGER_MALFORMED: 'windows_ledger_malformed',
WINDOWS_ID_NOT_FOUND: 'windows_id_not_found',
WINDOWS_ALREADY_RESOLVED: 'windows_already_resolved',
WINDOWS_WAIVE_REASON_EMPTY: 'windows_waive_reason_empty',
WINDOWS_INVALID_KIND: 'windows_invalid_kind',
WINDOWS_INVALID_FILE: 'windows_invalid_file',
WINDOWS_INVALID_TEXT: 'windows_invalid_text',
WINDOWS_INVALID_ID: 'windows_invalid_id',
WINDOWS_APPEND_MISSING_FIELD: 'windows_append_missing_field',
WINDOWS_USAGE: 'windows_usage',
});
/** Allowed window kinds. Aligned with the issue's enumerated sources. */
export const KINDS = Object.freeze([
'stub',
'todo',
'fixme',
'skipped-test',
'lint-warning',
'unmet-truth',
'unrun-verify',
'deviation',
]);
const KIND_SET = new Set(KINDS);
// ─── Types ─────────────────────────────────────────────────────────────────
export type WindowKind =
| 'stub'
| 'todo'
| 'fixme'
| 'skipped-test'
| 'lint-warning'
| 'unmet-truth'
| 'unrun-verify'
| 'deviation';
export type WindowStatus = 'open' | 'waived' | 'fixed';
export interface WindowEntry {
id: number;
kind: WindowKind;
phase: string;
file: string; // '' when not applicable
line: number | null; // null when not applicable
description: string;
status: WindowStatus;
reason: string; // '' unless status === 'waived'
recorded_at: string; // ISO-8601
resolved_at: string | null;
}
/** Input shape for appendWindow — id/status/timestamps are assigned by the fn. */
export type WindowInput = Pick<WindowEntry, 'kind' | 'phase' | 'description'> &
Partial<Pick<WindowEntry, 'file' | 'line'>>;
export interface Ledger {
schema_version: number;
open_count: number;
waived_count: number;
fixed_count: number;
total_count: number;
last_updated: string;
entries: WindowEntry[];
}
// ─── Errors ────────────────────────────────────────────────────────────────
/**
* Error carrying a REASON code. gsd-tools.cjs's `--json-errors` mode catches
* this and emits `{ ok: false, reason: err.reason, message: err.message }` to
* stderr; otherwise the message goes to stderr as plain text and the exit
* code is non-zero.
*/
export class WindowsError extends Error {
reason: string;
constructor(reason: string, message: string) {
super(message);
this.name = 'WindowsError';
this.reason = reason;
}
}
// ─── Pure: constructors + counts ───────────────────────────────────────────
export function emptyLedger(now: string): Ledger {
return {
schema_version: SCHEMA_VERSION,
open_count: 0,
waived_count: 0,
fixed_count: 0,
total_count: 0,
last_updated: now,
entries: [],
};
}
export function openCount(ledger: Ledger): number {
return ledger.open_count;
}
export function findByStatus(ledger: Ledger, status: WindowStatus): WindowEntry[] {
return ledger.entries.filter((e) => e.status === status);
}
function recomputeCounts(ledger: Ledger): Ledger {
let open = 0, waived = 0, fixed = 0;
for (const e of ledger.entries) {
if (e.status === 'open') open++;
else if (e.status === 'waived') waived++;
else if (e.status === 'fixed') fixed++;
}
return {
...ledger,
open_count: open,
waived_count: waived,
fixed_count: fixed,
total_count: ledger.entries.length,
};
}
function validateKind(kind: unknown): asserts kind is WindowKind {
if (typeof kind !== 'string' || !KIND_SET.has(kind)) {
throw new WindowsError(
REASON.WINDOWS_INVALID_KIND,
`Invalid window kind: ${JSON.stringify(kind)}. Allowed: ${KINDS.join(', ')}.`,
);
}
}
function validateDescription(description: unknown): string {
if (typeof description !== 'string' || description.trim() === '') {
throw new WindowsError(
REASON.WINDOWS_APPEND_MISSING_FIELD,
'Window description must be a non-empty string.',
);
}
rejectBacktickRun(description, 'description');
return description;
}
/**
* Reject any string field that contains a 4-backtick run. The ledger's JSON
* code block uses a 4-backtick fence; a 4-backtick run inside stringified
* entry text would terminate the fence early and brick the next parse
* (issue #1950 review H1). JSON.stringify does not escape backticks, so we
* must catch them at validate time.
*/
function rejectBacktickRun(value: string, field: string): void {
if (value.includes(FORBIDDEN_BACKTICK_RUN)) {
throw new WindowsError(
REASON.WINDOWS_INVALID_TEXT,
`Window ${field} contains a 4-backtick run, which would corrupt the ledger's JSON code fence.`,
);
}
}
function validateFile(file: unknown): string {
if (file == null || file === '') return '';
if (typeof file !== 'string') {
throw new WindowsError(
REASON.WINDOWS_INVALID_FILE,
'Window file must be a string when provided.',
);
}
// Reject path traversal — the ledger is a project-local artifact; absolute or
// parent-escaping paths serve no legitimate purpose and could mislead a human
// reviewer into investigating the wrong location. Reject NUL bytes too.
if (file.includes('\0')) {
throw new WindowsError(
REASON.WINDOWS_INVALID_FILE,
'Window file contains a NUL byte.',
);
}
if (path.isAbsolute(file) || /(^|[/\\])\.\.([/\\]|$)/.test(file)) {
throw new WindowsError(
REASON.WINDOWS_INVALID_FILE,
`Window file rejects path traversal/absolute paths: ${file}`,
);
}
return file;
}
function validateLine(line: unknown): number | null {
if (line == null || line === '') return null;
// Strict: number or numeric string only; reject garbage like "abc" (which
// Number() would silently coerce to NaN → null, hiding type drift). Issue
// #1950 review M2.
const n = typeof line === 'number' ? line : Number(line);
if (!Number.isInteger(n) || n < 1) {
throw new WindowsError(
REASON.WINDOWS_APPEND_MISSING_FIELD,
`Window line must be a positive integer when provided (got: ${JSON.stringify(line)}).`,
);
}
return n;
}
function nextId(entries: WindowEntry[]): number {
let max = 0;
for (const e of entries) if (e.id > max) max = e.id;
return max + 1;
}
/**
* Append a window to the ledger. Assigns the next dense id (max+1), sets
* status=open, timestamps via opts.now.
*
* Concurrency (issue #1950 review L2): NOT safe for concurrent writers. Two
* parallel `gsd_run windows append` invocations both read the same snapshot,
* both compute the same nextId, both write — the second atomic rename wins
* and the first append (and the entry it added) is silently lost. This is
* acceptable in the current single-executor-per-phase model; document if the
* executor ever gains parallel wave-level append.
*/
export function appendWindow(
ledger: Ledger,
input: WindowInput,
opts: { now: string } = { now: new Date().toISOString() },
): { ledger: Ledger; entry: WindowEntry } {
validateKind(input.kind);
const description = validateDescription(input.description);
const file = validateFile(input.file);
const line = validateLine(input.line);
const id = nextId(ledger.entries);
const entry: WindowEntry = {
id,
kind: input.kind,
phase: String(input.phase ?? ''),
file,
line,
description,
status: 'open',
reason: '',
recorded_at: opts.now,
resolved_at: null,
};
const entries = [...ledger.entries, entry];
const result = recomputeCounts({ ...ledger, entries, last_updated: opts.now });
return { ledger: result, entry };
}
function findEntryOrFail(ledger: Ledger, id: number): WindowEntry {
const entry = ledger.entries.find((e) => e.id === id);
if (!entry) {
throw new WindowsError(
REASON.WINDOWS_ID_NOT_FOUND,
`No window with id ${id}.`,
);
}
return entry;
}
function assertOpen(entry: WindowEntry): void {
if (entry.status !== 'open') {
throw new WindowsError(
REASON.WINDOWS_ALREADY_RESOLVED,
`Window ${entry.id} is already ${entry.status} (resolved_at=${entry.resolved_at}).`,
);
}
}
export function markWaived(
ledger: Ledger,
id: number,
reason: string,
opts: { now: string } = { now: new Date().toISOString() },
): Ledger {
if (typeof reason !== 'string' || reason.trim() === '') {
throw new WindowsError(
REASON.WINDOWS_WAIVE_REASON_EMPTY,
'Waive requires a non-empty recorded reason.',
);
}
const entry = findEntryOrFail(ledger, id);
assertOpen(entry);
const newStatus: WindowStatus = 'waived';
const entries = ledger.entries.map((e) =>
e.id === id
? { ...e, status: newStatus, reason, resolved_at: opts.now }
: e,
);
return recomputeCounts({ ...ledger, entries, last_updated: opts.now });
}
export function markFixed(
ledger: Ledger,
id: number,
opts: { now: string } = { now: new Date().toISOString() },
): Ledger {
const entry = findEntryOrFail(ledger, id);
assertOpen(entry);
const newStatus: WindowStatus = 'fixed';
const entries = ledger.entries.map((e) =>
e.id === id
? { ...e, status: newStatus, resolved_at: opts.now }
: e,
);
return recomputeCounts({ ...ledger, entries, last_updated: opts.now });
}
// ─── Pure: parse / render ──────────────────────────────────────────────────
// JSON-FENCE strategy (issue #1950 review H1): a description containing the
// 3-backtick markdown fence sequence would terminate the code block early
// inside JSON.stringify output (which does not escape backticks), corrupting
// the file and bricking the next parse. We use a 4-backtick fence which
// cannot collide with anything JSON.stringify can emit on its own (JSON has
// no 4-backtick operator), AND validate that no entry's text fields contain
// a 4-backtick run, so the rendered file is provably reparseable.
const JSON_FENCE_OPEN = '````json';
const JSON_FENCE_CLOSE = '````';
const FORBIDDEN_BACKTICK_RUN = '````';
/**
* Minimal strict frontmatter parser for flat scalar keys. Only supports the
* shape this module emits: `key: <number|string>` per line. Throws on any
* structural deviation — fail-closed on drift.
*/
function parseFrontmatterStrict(raw: string): Record<string, number | string> {
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
'Ledger missing frontmatter opening ---',
);
}
const headerEnd = raw.startsWith('---\r\n') ? 5 : 4;
const closeIdx = raw.indexOf('\n---', headerEnd);
if (closeIdx === -1) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
'Ledger missing frontmatter closing ---',
);
}
const yamlBody = raw.slice(headerEnd, closeIdx);
const out: Record<string, number | string> = {};
for (const line of yamlBody.split(/\r?\n/)) {
if (line.trim() === '') continue;
const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
if (!m) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger frontmatter line is not key: value: ${JSON.stringify(line)}`,
);
}
const [, key, valueStr] = m;
const trimmed = valueStr.trim();
if (/^-?\d+$/.test(trimmed)) {
out[key] = Number(trimmed);
} else if (/^-?\d+\.\d+$/.test(trimmed)) {
out[key] = Number(trimmed);
} else {
// String — strip surrounding quotes if present.
out[key] =
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
? trimmed.slice(1, -1)
: trimmed;
}
}
return out;
}
function parseJsonBlock(raw: string): WindowEntry[] {
const start = raw.indexOf(JSON_FENCE_OPEN);
if (start === -1) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
'Ledger missing JSON code block for entries.',
);
}
const end = raw.indexOf(JSON_FENCE_CLOSE, start + JSON_FENCE_OPEN.length);
if (end === -1) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
'Ledger JSON code block not terminated.',
);
}
const jsonText = raw.slice(start + JSON_FENCE_OPEN.length, end).trim();
let parsed: unknown;
try {
parsed = JSON.parse(jsonText);
} catch (e) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger JSON block failed to parse: ${(e as Error).message}`,
);
}
if (!Array.isArray(parsed)) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
'Ledger JSON block must be an array.',
);
}
return parsed.map(validateEntryShape);
}
function validateEntryShape(e: unknown, i: number): WindowEntry {
if (typeof e !== 'object' || e === null) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} is not an object.`,
);
}
const o = e as Record<string, unknown>;
const required = ['id', 'kind', 'phase', 'file', 'description', 'status', 'reason', 'recorded_at'];
for (const k of required) {
if (!(k in o)) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} missing required field: ${k}`,
);
}
}
if (typeof o.id !== 'number' || !Number.isInteger(o.id) || o.id < 1) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} has invalid id.`,
);
}
if (typeof o.kind !== 'string' || !KIND_SET.has(o.kind)) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} has invalid kind: ${JSON.stringify(o.kind)}`,
);
}
if (typeof o.status !== 'string' || !['open', 'waived', 'fixed'].includes(o.status)) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} has invalid status: ${JSON.stringify(o.status)}`,
);
}
if (typeof o.description !== 'string' || typeof o.reason !== 'string') {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger entry ${i} has non-string description/reason.`,
);
}
const phaseStr = typeof o.phase === 'string'
? o.phase
: (o.phase == null ? '' : typeof o.phase === 'number' || typeof o.phase === 'boolean' ? String(o.phase) : '');
const recordedStr = typeof o.recorded_at === 'string'
? o.recorded_at
: (o.recorded_at == null ? '' : typeof o.recorded_at === 'number' || typeof o.recorded_at === 'boolean' ? String(o.recorded_at) : '');
const resolvedStr = typeof o.resolved_at === 'string'
? o.resolved_at
: (o.resolved_at == null ? null : typeof o.resolved_at === 'number' || typeof o.resolved_at === 'boolean' ? String(o.resolved_at) : null);
return {
id: o.id,
kind: o.kind as WindowKind,
phase: phaseStr,
file: typeof o.file === 'string' ? o.file : '',
line: o.line == null ? null : (Number(o.line) || null),
description: o.description,
status: o.status as WindowStatus,
reason: o.reason,
recorded_at: recordedStr,
resolved_at: resolvedStr,
};
}
export function parseLedger(raw: string): Ledger {
const fm = parseFrontmatterStrict(raw);
if (fm.schema_version !== SCHEMA_VERSION) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger schema_version must be ${SCHEMA_VERSION}; got ${JSON.stringify(fm.schema_version)}.`,
);
}
const requiredCounts = ['open_count', 'waived_count', 'fixed_count', 'total_count'];
for (const k of requiredCounts) {
const v = fm[k];
if (typeof v !== 'number' || !Number.isInteger(v)) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger ${k} must be an integer; got ${JSON.stringify(v)}.`,
);
}
}
if (typeof fm.last_updated !== 'string') {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger last_updated must be a string; got ${JSON.stringify(fm.last_updated)}.`,
);
}
const entries = parseJsonBlock(raw);
const ledger: Ledger = {
schema_version: SCHEMA_VERSION,
open_count: typeof fm.open_count === 'number' ? fm.open_count : 0,
waived_count: typeof fm.waived_count === 'number' ? fm.waived_count : 0,
fixed_count: typeof fm.fixed_count === 'number' ? fm.fixed_count : 0,
total_count: typeof fm.total_count === 'number' ? fm.total_count : 0,
last_updated: typeof fm.last_updated === 'string' ? fm.last_updated : '',
entries,
};
// Cross-check: frontmatter counts must agree with entries-derived counts.
const recomputed = recomputeCounts(ledger);
if (
recomputed.open_count !== ledger.open_count ||
recomputed.waived_count !== ledger.waived_count ||
recomputed.fixed_count !== ledger.fixed_count ||
recomputed.total_count !== ledger.total_count
) {
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Ledger counts disagree with entries: frontmatter open/waived/fixed/total=` +
`${ledger.open_count}/${ledger.waived_count}/${ledger.fixed_count}/${ledger.total_count}` +
` but entries yield ${recomputed.open_count}/${recomputed.waived_count}/${recomputed.fixed_count}/${recomputed.total_count}.`,
);
}
return ledger;
}
export function renderLedger(ledger: Ledger): string {
const fm = [
'---',
`schema_version: ${ledger.schema_version}`,
`open_count: ${ledger.open_count}`,
`waived_count: ${ledger.waived_count}`,
`fixed_count: ${ledger.fixed_count}`,
`total_count: ${ledger.total_count}`,
`last_updated: ${ledger.last_updated}`,
'---',
'',
].join('\n');
const header = [
'# Broken Windows Ledger',
'',
'> Cross-phase defect register. `/gsd-ship` blocks while `open_count > 0`.',
'> Waive with `gsd-tools windows waive <id> "<reason>"` (reason required).',
'> Mark fixed with `gsd-tools windows fixed <id>`.',
'',
].join('\n');
const table = renderTable(ledger.entries);
const jsonBlock = [JSON_FENCE_OPEN, JSON.stringify(ledger.entries, null, 2), JSON_FENCE_CLOSE, ''].join('\n');
return [fm, header, table, '', jsonBlock].join('\n');
}
function renderTable(entries: WindowEntry[]): string {
if (entries.length === 0) {
return [
'| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |',
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
'| _(none)_ | | | | | _No windows recorded._ | | | | |',
].join('\n');
}
const rows = [
'| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |',
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
];
for (const e of entries) {
// Escape backslash FIRST, then pipe — markdown table cells treat `\` as
// the escape introducer, so a description containing `\|` would render
// as an escaped pipe (i.e. a literal `|` inside the cell) and split the
// column. Escaping `\` → `\\` first makes the subsequent `\|` replacement
// unambiguous. (CodeQL: js/incomplete-sanitization — issue #1950 PR #2441.)
const cell = (s: string | number | null) =>
String(s ?? '')
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|');
rows.push(
[
'|', cell(e.id), '|', cell(e.phase), '|', cell(e.kind), '|',
cell(e.file), '|', cell(e.line ?? ''), '|',
cell(e.description), '|', cell(e.status), '|',
cell(e.reason), '|', cell(e.recorded_at), '|', cell(e.resolved_at), '|',
].join(' '),
);
}
return rows.join('\n');
}
// ─── I/O entry points ──────────────────────────────────────────────────────
function ledgerPath(cwd: string): string {
return path.join(cwd, '.planning', LEDGER_FILE_NAME);
}
function readLedgerOrNull(cwd: string): Ledger | null {
const p = ledgerPath(cwd);
let raw: string;
try {
raw = fs.readFileSync(p, 'utf8');
} catch (e: unknown) {
// ENOENT is the only "no ledger yet" case. Every other fs error (EACCES,
// EPERM, EIO, ENOTDIR, EBADF, ...) must NOT be silently coerced to "empty
// ledger" — that would fail the ship gate OPEN on an unreadable ledger,
// contradicting the workflow's documented "fail closed on unreadable"
// invariant (issue #1950 review H2). Propagate as malformed so the gate
// blocks and the operator sees a real diagnostic.
const code = (e && typeof e === 'object' && 'code' in e)
? String((e as { code?: unknown }).code)
: '';
if (code === 'ENOENT') return null;
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Could not read ledger at ${p} (${code || 'unknown fs error'}): ${(e as Error).message}.`,
);
}
// parseLedger throws WindowsError on malformed content — caller surfaces it.
return parseLedger(raw);
}
function ensurePlanningDir(cwd: string): void {
const dir = path.join(cwd, '.planning');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Errnos that Windows throws transiently on rename when a reader or antivirus
* scanner holds the target. We retry through these; anything else propagates.
*
* NOTE (issue #1950 review L3): the retry uses a short busy-wait rather than
* setTimeout — this is a synchronous CLI path with no event loop to yield on,
* and the cumulative wait is bounded at 25+50+100+200 = 375ms across 5 attempts.
* If a future caller moves this onto an async path, swap to awaitable sleeps.
*/
const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
const RENAME_MAX_ATTEMPTS = 5;
const RENAME_BACKOFF_MS = 25;
function renameWithRetry(tmp: string, target: string): void {
let lastErr: unknown;
for (let attempt = 0; attempt < RENAME_MAX_ATTEMPTS; attempt++) {
try {
fs.renameSync(tmp, target);
return;
} catch (err: unknown) {
lastErr = err;
const code = (err && typeof err === 'object' && 'code' in err) ? String((err as { code?: unknown }).code) : '';
if (code && RENAME_RETRY_ERRNOS.has(code) && attempt < RENAME_MAX_ATTEMPTS - 1) {
// Exponential-ish backoff: 25ms, 50ms, 100ms, 200ms.
const delay = RENAME_BACKOFF_MS * Math.pow(2, attempt);
const start = Date.now();
while (Date.now() - start < delay) {
// Busy-wait a very short time — Windows transient locks usually clear in <100ms.
}
continue;
}
throw err;
}
}
throw lastErr;
}
function writeLedgerAtomic(cwd: string, ledger: Ledger): void {
ensurePlanningDir(cwd);
const p = ledgerPath(cwd);
const tmp = `${p}.${process.pid}.tmp`;
fs.writeFileSync(tmp, renderLedger(ledger), 'utf8');
try {
renameWithRetry(tmp, p);
} catch (err) {
// Clean up the orphaned tmp file so repeated failures don't accumulate
// `.planning/WINDOWS.md.<pid>.tmp` files (issue #1950 review M1). Best-effort:
// unlink failures (e.g., already gone) are swallowed.
try { fs.unlinkSync(tmp); } catch { /* best-effort cleanup */ }
throw err;
}
}
function nowIso(): string {
return new Date().toISOString();
}
/** Emit a JSON result to stdout in the canonical shape. */
function emit(obj: unknown): void {
process.stdout.write(JSON.stringify(obj, null, 2));
}
/** `gsd-tools windows status [--raw]`. */
export function cmdWindowsStatus(cwd: string, opts: { raw?: boolean } = {}): void {
let ledger: Ledger;
try {
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
} catch (e) {
if (e instanceof WindowsError) throw e;
throw new WindowsError(
REASON.WINDOWS_LEDGER_MALFORMED,
`Unexpected error reading ledger: ${(e as Error).message}`,
);
}
void opts; // status output is JSON in both human and raw modes (single shape)
emit({ ok: true, ledger });
}
/** `gsd-tools windows append --kind K --phase N [--file F] [--line L] --description D`. */
export function cmdWindowsAppend(
cwd: string,
args: string[],
opts: { raw?: boolean } = {},
): void {
void opts;
const parsed = parseArgs(args, {
flags: ['--kind', '--phase', '--file', '--line', '--description'],
required: ['--kind', '--phase', '--description'],
});
let ledger: Ledger;
try {
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
} catch (e) {
if (e instanceof WindowsError) throw e;
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
}
const result = appendWindow(
ledger,
{
kind: parsed.values['--kind'] as WindowKind,
phase: parsed.values['--phase'] ?? '',
file: parsed.values['--file'] ?? '',
line: parsed.values['--line'] == null ? null : Number(parsed.values['--line']),
description: parsed.values['--description'] ?? '',
},
{ now: nowIso() },
);
writeLedgerAtomic(cwd, result.ledger);
emit({ ok: true, ledger: result.ledger, entry: result.entry });
}
/** `gsd-tools windows waive <id> "<reason>"`. */
export function cmdWindowsWaive(
cwd: string,
args: string[],
opts: { raw?: boolean } = {},
): void {
void opts;
const { positionals } = parseArgs(args, { flags: [], required: [], positionals: 2 });
const idStr = positionals[0];
const reason = positionals[1];
const id = parseIdOrThrow(idStr);
let ledger: Ledger;
try {
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
} catch (e) {
if (e instanceof WindowsError) throw e;
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
}
const updated = markWaived(ledger, id, reason ?? '', { now: nowIso() });
writeLedgerAtomic(cwd, updated);
emit({ ok: true, ledger: updated });
}
/** `gsd-tools windows fixed <id>`. */
export function cmdWindowsMarkFixed(
cwd: string,
args: string[],
opts: { raw?: boolean } = {},
): void {
void opts;
const { positionals } = parseArgs(args, { flags: [], required: [], positionals: 1 });
const id = parseIdOrThrow(positionals[0]);
let ledger: Ledger;
try {
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
} catch (e) {
if (e instanceof WindowsError) throw e;
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
}
const updated = markFixed(ledger, id, { now: nowIso() });
writeLedgerAtomic(cwd, updated);
emit({ ok: true, ledger: updated });
}
function parseIdOrThrow(raw: string | undefined): number {
if (raw == null || raw === '') {
throw new WindowsError(
REASON.WINDOWS_INVALID_ID,
'Window id is required.',
);
}
const n = Number(raw);
if (!Number.isInteger(n) || n < 1) {
throw new WindowsError(
REASON.WINDOWS_INVALID_ID,
`Window id must be a positive integer (got: ${JSON.stringify(raw)}).`,
);
}
return n;
}
/** Minimal argv parser — flag values via `--flag value` or `--flag=value`. */
function parseArgs(
args: string[],
spec: { flags: string[]; required: string[]; positionals?: number },
): { values: Record<string, string | undefined>; positionals: string[] } {
const values: Record<string, string | undefined> = {};
const positionals: string[] = [];
const flagSet = new Set(spec.flags);
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a == null) continue;
if (a.startsWith('--')) {
const eq = a.indexOf('=');
const flagName = eq === -1 ? a : a.slice(0, eq);
if (!flagSet.has(flagName)) {
throw new WindowsError(
REASON.WINDOWS_USAGE,
`Unknown flag: ${flagName}`,
);
}
if (eq !== -1) {
values[flagName] = a.slice(eq + 1);
} else {
const next = args[i + 1];
if (next == null || next.startsWith('--')) {
if (!(flagName in values)) values[flagName] = undefined;
} else {
values[flagName] = next;
i++;
}
}
} else {
positionals.push(a);
}
}
for (const r of spec.required) {
if (values[r] == null || values[r] === '') {
throw new WindowsError(
REASON.WINDOWS_USAGE,
`Missing required flag: ${r}`,
);
}
}
const want = spec.positionals ?? 0;
if (positionals.length < want) {
throw new WindowsError(
REASON.WINDOWS_USAGE,
`Expected ${want} positional argument(s); got ${positionals.length}.`,
);
}
return { values, positionals };
}

View File

@@ -62,7 +62,7 @@ const UNMUTATED = [
// Full test command used by local runs and as the fallback when CI does not
// inject a per-shard command via MUTATION_TEST_CMD.
// Keep this list in sync with the tests arrays in scripts/mutation-matrix.cjs COVERED.
const DEFAULT_TEST_CMD = 'node --test tests/context-utilization.property.test.cjs tests/prompt-budget.property.test.cjs tests/frontmatter.property.test.cjs tests/adr-parser.property.test.cjs tests/config-schema.property.test.cjs tests/adr-parser.test.cjs tests/active-workstream-store.test.cjs tests/active-workstream-store.unit.test.cjs tests/prompt-budget.unit.test.cjs tests/adr-parser.unit.test.cjs tests/frontmatter.unit.test.cjs tests/core-utils.test.cjs';
const DEFAULT_TEST_CMD = 'node --test tests/context-utilization.property.test.cjs tests/prompt-budget.property.test.cjs tests/frontmatter.property.test.cjs tests/adr-parser.property.test.cjs tests/config-schema.property.test.cjs tests/adr-parser.test.cjs tests/active-workstream-store.test.cjs tests/active-workstream-store.unit.test.cjs tests/prompt-budget.unit.test.cjs tests/adr-parser.unit.test.cjs tests/frontmatter.unit.test.cjs tests/core-utils.test.cjs tests/broken-windows.test.cjs';
/** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
export default {

View File

@@ -14,7 +14,7 @@
"gsd-domain-researcher.md": 7032,
"gsd-eval-auditor.md": 12496,
"gsd-eval-planner.md": 7008,
"gsd-executor.md": 46644,
"gsd-executor.md": 47951,
"gsd-framework-selector.md": 6778,
"gsd-integration-checker.md": 15238,
"gsd-intel-updater.md": 18166,

View File

@@ -0,0 +1,725 @@
'use strict';
/**
* Broken-windows ledger — behavioral + property tests.
*
* Module: gsd-core/bin/lib/broken-windows.cjs (compiled from src/broken-windows.cts)
* CLI: gsd-tools windows <status|append|waive|fixed>
*
* Issue: #1950 — enforced cross-phase defect register gating /gsd-ship.
*
* Coverage map (acceptance criteria from #1950):
* - Executor writes stubs to ledger → append (CLI + pure)
* - /gsd-ship fails while any entry is open → openCount + cmdWindowsStatus
* - Waive requires non-empty reason → markWaived / cmdWindowsWaive
* - Marking fixed removes from blocking set → markFixed / cmdWindowsMarkFixed
* - Open-window count in progress surface → cmdWindowsStatus emits open_count
* - Tests cover all four + clean-on-empty → empty ledger + full lifecycle
*
* Hermetic: each CLI test uses its own tmpdir via createTempDir and cleans up
* via t.after() (CONTRIBUTING.md pattern 2). No shared state between tests.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup, runGsdTools } = require('./helpers.cjs');
const fc = require('./helpers/fast-check-setup.cjs');
const {
REASON,
WindowsError,
LEDGER_FILE_NAME,
emptyLedger,
parseLedger,
renderLedger,
appendWindow,
markWaived,
markFixed,
openCount,
} = require('../gsd-core/bin/lib/broken-windows.cjs');
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
/** Construct a minimal valid WindowEntry input for tests. */
function makeEntry(overrides = {}) {
return {
kind: 'stub',
phase: '2',
description: 'hardcoded empty list',
...overrides,
};
}
/** Validator: matches a WindowsError carrying a specific REASON code. */
function reasonIs(code) {
return (err) => err instanceof WindowsError && err.reason === code;
}
// ---------------------------------------------------------------------------
// Pure: emptyLedger + openCount
// ---------------------------------------------------------------------------
describe('broken-windows: emptyLedger + openCount', () => {
test('emptyLedger returns a ledger with zero counts and schema_version 1', () => {
const led = emptyLedger('2026-07-19T00:00:00Z');
assert.equal(led.schema_version, 1);
assert.equal(led.open_count, 0);
assert.equal(led.waived_count, 0);
assert.equal(led.fixed_count, 0);
assert.equal(led.total_count, 0);
assert.equal(led.last_updated, '2026-07-19T00:00:00Z');
assert.deepEqual(led.entries, []);
});
test('openCount of empty ledger is 0 (clean-ship baseline)', () => {
assert.equal(openCount(emptyLedger('now')), 0);
});
});
// ---------------------------------------------------------------------------
// Pure: appendWindow
// ---------------------------------------------------------------------------
describe('broken-windows: appendWindow', () => {
test('appending to an empty ledger assigns id=1, status=open, records timestamps', () => {
const led0 = emptyLedger('2026-07-19T00:00:00Z');
const { ledger, entry } = appendWindow(led0, makeEntry(), { now: '2026-07-19T12:00:00Z' });
assert.equal(entry.id, 1);
assert.equal(entry.status, 'open');
assert.equal(entry.recorded_at, '2026-07-19T12:00:00Z');
assert.equal(entry.resolved_at, null);
assert.equal(ledger.open_count, 1);
assert.equal(ledger.total_count, 1);
assert.equal(ledger.last_updated, '2026-07-19T12:00:00Z');
});
test('second append gets id=2 (ids are dense and monotonic)', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry({ description: 'first' }), { now: 't1' }));
({ ledger: led } = appendWindow(led, makeEntry({ description: 'second' }), { now: 't2' }));
assert.equal(led.entries[0].id, 1);
assert.equal(led.entries[1].id, 2);
assert.equal(led.total_count, 2);
assert.equal(openCount(led), 2);
});
test('append rejects unknown kind (fail-closed on schema drift)', () => {
const led = emptyLedger('now');
assert.throws(
() => appendWindow(led, makeEntry({ kind: 'bogus' })),
reasonIs(REASON.WINDOWS_INVALID_KIND),
);
});
test('append rejects empty description (no vacuous windows)', () => {
const led = emptyLedger('now');
assert.throws(
() => appendWindow(led, makeEntry({ description: '' })),
reasonIs(REASON.WINDOWS_APPEND_MISSING_FIELD),
);
assert.throws(
() => appendWindow(led, makeEntry({ description: ' ' })),
reasonIs(REASON.WINDOWS_APPEND_MISSING_FIELD),
);
});
test('append rejects path-traversal in --file (security boundary)', () => {
const led = emptyLedger('now');
assert.throws(
() => appendWindow(led, makeEntry({ file: '../../etc/passwd' })),
reasonIs(REASON.WINDOWS_INVALID_FILE),
);
});
test('append rejects 4-backtick run in description (H1 regression — would brick the JSON fence)', () => {
const led = emptyLedger('now');
assert.throws(
() => appendWindow(led, makeEntry({ description: 'see ```` four backticks' })),
reasonIs(REASON.WINDOWS_INVALID_TEXT),
);
// 3-backtick run is fine — the fence is 4-tick so 3-tick content is safe.
const led2 = emptyLedger('now');
const { ledger } = appendWindow(led2, makeEntry({ description: 'see ```js``` inline' }), { now: 't' });
assert.equal(ledger.entries[0].description, 'see ```js``` inline');
// And reparses cleanly:
assert.doesNotThrow(() => parseLedger(renderLedger(ledger)));
});
test('renderTable escapes backslash before pipe (CodeQL: incomplete-sanitization — PR #2441)', () => {
// A description containing `\|` must NOT split the markdown table cell.
// Escape order: `\` → `\\` first, then `|` → `\|`. If pipe is escaped first,
// `\|` in input becomes `\\|` in output which markdown renders as `\` + cell-sep.
const led0 = emptyLedger('2026-07-19T00:00:00Z');
const { ledger } = appendWindow(
led0,
makeEntry({ description: 'path with \\| separator and | pipe and \\ backslash' }),
{ now: '2026-07-19T12:00:00Z' },
);
const rendered = renderLedger(ledger);
// The JSON block (source of truth) preserves the description verbatim and reparses.
const reparsed = parseLedger(rendered);
assert.equal(reparsed.entries[0].description, 'path with \\| separator and | pipe and \\ backslash');
// The table row for this entry has exactly 10 cells (one per column). Counting
// unescaped pipes inside the row would surface a split. The cell's rendered
// form is `path with \\| separator and \| pipe and \\ backslash` — every pipe
// is preceded by a backslash, so splitting on /(?<!\\)\|/ yields 10 cells.
const tableLine = rendered.split('\n').find((l) => l.includes('path with'));
assert.ok(tableLine, 'table row for the test entry must exist');
// Walk the line and count pipes that are NOT preceded by a backslash.
let unescapedPipes = 0;
for (let i = 0; i < tableLine.length; i++) {
if (tableLine[i] === '|' && tableLine[i - 1] !== '\\') unescapedPipes++;
}
// 10 cells = 11 cell-separator pipes per row (leading + 9 internal + trailing).
assert.equal(unescapedPipes, 11, 'table row must have exactly 11 unescaped pipes (10 cells) — backslash-pipe in description must NOT add a split');
});
});
// ---------------------------------------------------------------------------
// Pure: markWaived (acceptance: waive requires non-empty reason)
// ---------------------------------------------------------------------------
describe('broken-windows: markWaived', () => {
test('waive with non-empty reason succeeds; waived_count increments; open_count decrements', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
led = markWaived(led, 1, 'Manual QA covers it', { now: 't2' });
assert.equal(led.entries[0].status, 'waived');
assert.equal(led.entries[0].reason, 'Manual QA covers it');
assert.equal(led.entries[0].resolved_at, 't2');
assert.equal(led.open_count, 0);
assert.equal(led.waived_count, 1);
assert.equal(openCount(led), 0); // waived does not block
});
test('waive with empty reason throws (boundary: limit-1 = 0 chars)', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
assert.throws(
() => markWaived(led, 1, ''),
reasonIs(REASON.WINDOWS_WAIVE_REASON_EMPTY),
);
});
test('waive with whitespace-only reason throws (boundary: limit = spaces)', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
assert.throws(
() => markWaived(led, 1, ' '),
reasonIs(REASON.WINDOWS_WAIVE_REASON_EMPTY),
);
});
test('waive with single-char reason succeeds (boundary: limit+1 = 1 char)', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
led = markWaived(led, 1, 'x', { now: 't2' });
assert.equal(led.entries[0].status, 'waived');
});
test('waive unknown id throws', () => {
const led = emptyLedger('now');
assert.throws(
() => markWaived(led, 999, 'reason'),
reasonIs(REASON.WINDOWS_ID_NOT_FOUND),
);
});
test('waive on already-resolved entry throws (no double-resolution)', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
led = markFixed(led, 1, { now: 't2' });
assert.throws(
() => markWaived(led, 1, 'late', { now: 't3' }),
reasonIs(REASON.WINDOWS_ALREADY_RESOLVED),
);
});
});
// ---------------------------------------------------------------------------
// Pure: markFixed (acceptance: fixed removes from blocking set)
// ---------------------------------------------------------------------------
describe('broken-windows: markFixed', () => {
test('fixed decrements open_count and increments fixed_count', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
led = markFixed(led, 1, { now: 't2' });
assert.equal(led.entries[0].status, 'fixed');
assert.equal(led.entries[0].resolved_at, 't2');
assert.equal(led.open_count, 0);
assert.equal(led.fixed_count, 1);
assert.equal(openCount(led), 0);
});
test('fixed on unknown id throws', () => {
const led = emptyLedger('now');
assert.throws(
() => markFixed(led, 999),
reasonIs(REASON.WINDOWS_ID_NOT_FOUND),
);
});
test('fixed on already-resolved throws', () => {
let led = emptyLedger('now');
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
led = markWaived(led, 1, 'have it', { now: 't2' });
assert.throws(
() => markFixed(led, 1, { now: 't3' }),
reasonIs(REASON.WINDOWS_ALREADY_RESOLVED),
);
});
});
// ---------------------------------------------------------------------------
// Pure: parseLedger / renderLedger roundtrip (property test, fast-check)
// ---------------------------------------------------------------------------
describe('broken-windows: parse/render roundtrip property', () => {
const arbKind = fc.constantFrom('stub', 'todo', 'fixme', 'skipped-test', 'lint-warning', 'unmet-truth', 'unrun-verify', 'deviation');
const arbStatus = fc.constantFrom('open', 'waived', 'fixed');
const arbPhase = fc.integer({ min: 1, max: 99 }).map(n => String(n));
const arbText = fc.string({ minLength: 1, maxLength: 80 }).map(s => s.replace(/[\r\n\t|]/g, ' ').trim() || 'x');
const arbEntry = fc.record({
id: fc.integer({ min: 1, max: 1000 }),
kind: arbKind,
phase: arbPhase,
description: arbText,
status: arbStatus,
}).map((e) => ({
id: e.id,
kind: e.kind,
phase: e.phase,
file: e.id % 2 === 0 ? '' : `src/file${e.id}.ts`,
line: e.id % 2 === 0 ? null : e.id * 10,
description: e.description,
status: e.status,
reason: e.status === 'waived' ? 'justified' : '',
recorded_at: '2026-07-19T00:00:00Z',
resolved_at: e.status === 'open' ? null : '2026-07-19T01:00:00Z',
}));
const arbLedger = fc.array(arbEntry, { maxLength: 6 }).map((entries) => {
const open = entries.filter(e => e.status === 'open').length;
const waived = entries.filter(e => e.status === 'waived').length;
const fixed = entries.filter(e => e.status === 'fixed').length;
return {
schema_version: 1,
open_count: open,
waived_count: waived,
fixed_count: fixed,
total_count: entries.length,
last_updated: '2026-07-19T00:00:00Z',
entries,
};
});
test('property: render(parse(render(ledger))) === render(ledger)', () => {
fc.assert(fc.property(arbLedger, (ledger) => {
const rendered1 = renderLedger(ledger);
const parsed = parseLedger(rendered1);
const rendered2 = renderLedger(parsed);
assert.equal(rendered2, rendered1, 'roundtrip must be stable');
}));
});
test('property: parseLedger never hangs or crashes on arbitrary unicode strings', () => {
fc.assert(fc.property(fc.string({ maxLength: 200 }), (raw) => {
try { parseLedger(raw); } catch { /* malformed input is allowed to throw */ }
}));
});
});
// ---------------------------------------------------------------------------
// Pure: parseLedger fail-closed on malformed input
// ---------------------------------------------------------------------------
describe('broken-windows: parseLedger fail-closed', () => {
test('rejects frontmatter with wrong schema_version', () => {
const raw = [
'---',
'schema_version: 99',
'open_count: 0',
'waived_count: 0',
'fixed_count: 0',
'total_count: 0',
'last_updated: 2026-07-19T00:00:00Z',
'---',
'',
'```json',
'[]',
'```',
'',
].join('\n');
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
});
test('rejects frontmatter missing open_count', () => {
const raw = [
'---',
'schema_version: 1',
'---',
'',
'```json',
'[]',
'```',
'',
].join('\n');
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
});
test('rejects frontmatter with non-numeric open_count', () => {
const raw = [
'---',
'schema_version: 1',
'open_count: "zero"',
'---',
'',
'```json',
'[]',
'```',
'',
].join('\n');
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
});
});
// ---------------------------------------------------------------------------
// CLI: gsd-tools windows status (acceptance: clean-ship on empty)
// ---------------------------------------------------------------------------
describe('broken-windows CLI: windows status', () => {
test('status on a project with no ledger returns open_count=0 (backward-compat baseline)', (t) => {
const tmp = createTempDir('bw-status-empty-');
t.after(() => cleanup(tmp));
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(res.success, true, `stderr: ${res.error || ''}`);
const obj = JSON.parse(res.output);
assert.equal(obj.ok, true);
assert.equal(obj.ledger.open_count, 0);
assert.deepEqual(obj.ledger.entries, []);
});
test('status on a malformed ledger fails closed', (t) => {
const tmp = createTempDir('bw-status-malformed-');
t.after(() => cleanup(tmp));
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', LEDGER_FILE_NAME),
'not valid markdown or frontmatter',
);
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(res.success, false);
assert.ok(res.exitCode !== 0);
assert.match(res.error, /malformed|invalid frontmatter|missing frontmatter/i);
});
test('status on an UNREADABLE ledger fails closed (H2 regression — EACCES must not be silently empty)', (t) => {
// Skip on Windows where chmod 000 doesn't apply to root/admin or where the FS
// ignores mode bits; CI lanes run as non-root so the EACCES path is real.
const tmp = createTempDir('bw-status-eacces-');
t.after(() => {
try { fs.chmodSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), 0o644); } catch { /* best-effort */ }
cleanup(tmp);
});
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
// A ledger with open_count=1 — if EACCES silently returned empty, ship gate would pass.
const validLedger = [
'---',
'schema_version: 1',
'open_count: 1',
'waived_count: 0',
'fixed_count: 0',
'total_count: 1',
'last_updated: 2026-07-19T00:00:00Z',
'---',
'',
'````json',
JSON.stringify([{
id: 1, kind: 'stub', phase: '2', file: '', line: null,
description: 'unreadable-test', status: 'open', reason: '',
recorded_at: 't', resolved_at: null,
}]),
'````',
'',
].join('\n');
fs.writeFileSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), validLedger);
try { fs.chmodSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), 0o000); } catch { return; }
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
// If the chmod actually took (non-root), the read must fail. If running as
// root (CI rarely does), the read may succeed — either way, the test must
// never see a false-green "open_count: 0" from a file we KNOW has open_count=1.
if (res.success) {
const obj = JSON.parse(res.output);
assert.notEqual(obj.ledger.open_count, 0, 'EACCES must NOT silently coerce an open_count=1 ledger to 0');
} else {
assert.match(res.error, /could not read|EACCES|malformed/i);
}
});
});
// ---------------------------------------------------------------------------
// CLI: gsd-tools windows append (acceptance: executor writes stubs)
// ---------------------------------------------------------------------------
describe('broken-windows CLI: windows append', () => {
test('append creates the ledger if absent and records the entry', (t) => {
const tmp = createTempDir('bw-append-create-');
t.after(() => cleanup(tmp));
const res = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2',
'--file', 'src/auth.ts', '--line', '42',
'--description', 'hardcoded empty list in UserService.list'],
tmp,
);
assert.equal(res.success, true, `stderr: ${res.error || ''}`);
const obj = JSON.parse(res.output);
assert.equal(obj.ok, true);
assert.equal(obj.entry.id, 1);
assert.equal(obj.entry.status, 'open');
assert.equal(obj.ledger.open_count, 1);
// File exists with the right frontmatter and is re-readable.
const ledgerPath = path.join(tmp, '.planning', LEDGER_FILE_NAME);
assert.equal(fs.existsSync(ledgerPath), true);
// Second invocation observes the persisted entry (idempotent read).
const res2 = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(res2.success, true);
const obj2 = JSON.parse(res2.output);
assert.equal(obj2.ledger.open_count, 1);
assert.equal(obj2.ledger.entries[0].id, 1);
});
test('append a second entry gets id=2', (t) => {
const tmp = createTempDir('bw-append-second-');
t.after(() => cleanup(tmp));
const r1 = runGsdTools(
['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'first todo'],
tmp,
);
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
const r2 = runGsdTools(
['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'second todo'],
tmp,
);
assert.equal(r2.success, true);
const obj2 = JSON.parse(r2.output);
assert.equal(obj2.entry.id, 2);
assert.equal(obj2.ledger.total_count, 2);
});
test('append rejects unknown kind', (t) => {
const tmp = createTempDir('bw-append-badkind-');
t.after(() => cleanup(tmp));
const res = runGsdTools(
['windows', 'append', '--kind', 'bogus', '--phase', '2', '--description', 'x'],
tmp,
);
assert.equal(res.success, false);
assert.match(res.error, /invalid kind|allowed:/i);
});
test('append rejects path-traversal in --file', (t) => {
const tmp = createTempDir('bw-append-traversal-');
t.after(() => cleanup(tmp));
const res = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2',
'--file', '../../etc/passwd', '--description', 'x'],
tmp,
);
assert.equal(res.success, false);
assert.match(res.error, /traversal|absolute|file/i);
});
test('append rejects missing description', (t) => {
const tmp = createTempDir('bw-append-nodesc-');
t.after(() => cleanup(tmp));
const res = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2'],
tmp,
);
assert.equal(res.success, false);
assert.match(res.error, /description|required|missing/i);
});
test('append --line boundary: 0 / 1 / large int (limit-1 / limit / limit+1)', (t) => {
const tmp = createTempDir('bw-append-line-bva-');
t.after(() => cleanup(tmp));
// line=1: smallest valid line — limit boundary.
const r1 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '1', '--description', 'b'], tmp);
assert.equal(r1.success, true, `--line 1 should succeed: ${r1.error || ''}`);
assert.equal(JSON.parse(r1.output).entry.line, 1);
// line=large: limit+1 boundary (just confirm it accepts arbitrary positive int).
const r2 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '999999', '--description', 'c'], tmp);
assert.equal(r2.success, true, `--line 999999 should succeed: ${r2.error || ''}`);
assert.equal(JSON.parse(r2.output).entry.line, 999999);
// line=0: limit-1 boundary — invalid (lines are 1-indexed; 0 is not a line).
// M2 fix: validateLine no longer treats 0 as omit; it rejects as non-positive.
const rZero = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '0', '--description', 'a'], tmp);
assert.equal(rZero.success, false, '--line 0 must fail (positive integers only)');
assert.match(rZero.error, /line|positive integer/i);
// line=-1 and line=abc: also invalid — fail closed.
const rNeg = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '-1', '--description', 'd'], tmp);
assert.equal(rNeg.success, false);
assert.match(rNeg.error, /line|positive integer/i);
const rGarbage = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', 'abc', '--description', 'e'], tmp);
assert.equal(rGarbage.success, false);
assert.match(rGarbage.error, /line|positive integer/i);
// line OMITTED entirely: valid, line is null.
const rOmit = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'f'], tmp);
assert.equal(rOmit.success, true, `--line omitted should succeed: ${rOmit.error || ''}`);
assert.equal(JSON.parse(rOmit.output).entry.line, null);
});
test('append rejects 4-backtick description via CLI (H1 regression)', (t) => {
const tmp = createTempDir('bw-append-4tick-');
t.after(() => cleanup(tmp));
const res = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'has ```` four backticks'],
tmp,
);
assert.equal(res.success, false);
assert.match(res.error, /4-backtick|fence|invalid_text/i);
});
});
// ---------------------------------------------------------------------------
// CLI: gsd-tools windows waive (acceptance: waive-with-reason)
// ---------------------------------------------------------------------------
describe('broken-windows CLI: windows waive', () => {
test('waive with reason succeeds; subsequent status reports open_count=0', (t) => {
const tmp = createTempDir('bw-waive-ok-');
t.after(() => cleanup(tmp));
const r1 = runGsdTools(
['windows', 'append', '--kind', 'skipped-test', '--phase', '3',
'--file', 'tests/x.test.cjs', '--line', '18',
'--description', 't.skip logout flow'],
tmp,
);
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
const r2 = runGsdTools(
['windows', 'waive', '1', 'Manual QA covers it; CI cannot reach logout URL'],
tmp,
);
assert.equal(r2.success, true, `stderr: ${r2.error || ''}`);
const obj = JSON.parse(r2.output);
assert.equal(obj.ok, true);
assert.equal(obj.ledger.entries[0].status, 'waived');
assert.equal(obj.ledger.entries[0].reason, 'Manual QA covers it; CI cannot reach logout URL');
const r3 = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(r3.success, true);
const status = JSON.parse(r3.output);
assert.equal(status.ledger.open_count, 0); // waived does not block ship
assert.equal(status.ledger.waived_count, 1);
});
test('waive with empty reason fails', (t) => {
const tmp = createTempDir('bw-waive-empty-');
t.after(() => cleanup(tmp));
const r1 = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'x'],
tmp,
);
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
const r2 = runGsdTools(['windows', 'waive', '1', ''], tmp);
assert.equal(r2.success, false);
assert.match(r2.error, /waive.*reason|non-empty|reason.*required/i);
});
test('waive unknown id fails', (t) => {
const tmp = createTempDir('bw-waive-unknown-');
t.after(() => cleanup(tmp));
const res = runGsdTools(['windows', 'waive', '999', 'because'], tmp);
assert.equal(res.success, false);
assert.match(res.error, /no window|id 999|not found/i);
});
});
// ---------------------------------------------------------------------------
// CLI: gsd-tools windows fixed (acceptance: fixed removes from blocking set)
// ---------------------------------------------------------------------------
describe('broken-windows CLI: windows fixed', () => {
test('fixed removes the entry from the blocking set', (t) => {
const tmp = createTempDir('bw-fixed-');
t.after(() => cleanup(tmp));
const r1 = runGsdTools(
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'x'],
tmp,
);
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
const rBefore = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(rBefore.success, true);
assert.equal(JSON.parse(rBefore.output).ledger.open_count, 1);
const r2 = runGsdTools(['windows', 'fixed', '1'], tmp);
assert.equal(r2.success, true, `stderr: ${r2.error || ''}`);
const obj = JSON.parse(r2.output);
assert.equal(obj.ledger.open_count, 0);
assert.equal(obj.ledger.fixed_count, 1);
assert.equal(obj.ledger.entries[0].status, 'fixed');
});
test('fixed on unknown id fails', (t) => {
const tmp = createTempDir('bw-fixed-unknown-');
t.after(() => cleanup(tmp));
const res = runGsdTools(['windows', 'fixed', '999'], tmp);
assert.equal(res.success, false);
assert.match(res.error, /no window|id 999|not found/i);
});
});
// ---------------------------------------------------------------------------
// CLI: full lifecycle — append → waive → append → fixed → clean ship
// ---------------------------------------------------------------------------
describe('broken-windows CLI: lifecycle', () => {
test('append two, waive one, fix one, then ship is clean', (t) => {
const tmp = createTempDir('bw-lifecycle-');
t.after(() => cleanup(tmp));
const r1 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'a'], tmp);
const r2 = runGsdTools(['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'b'], tmp);
const r3 = runGsdTools(['windows', 'waive', '1', 'deferred to follow-up'], tmp);
const r4 = runGsdTools(['windows', 'fixed', '2'], tmp);
assert.equal(r1.success && r2.success && r3.success && r4.success, true,
`lifecycle steps failed: r1=${r1.error || 'ok'} r2=${r2.error || 'ok'} r3=${r3.error || 'ok'} r4=${r4.error || 'ok'}`);
const rFinal = runGsdTools(['windows', 'status', '--raw'], tmp);
assert.equal(rFinal.success, true);
const status = JSON.parse(rFinal.output);
assert.equal(status.ledger.open_count, 0); // ship gate would pass
assert.equal(status.ledger.waived_count, 1);
assert.equal(status.ledger.fixed_count, 1);
assert.equal(status.ledger.total_count, 2);
});
});

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "1db46cac3f4d9889",
"agents/gsd-eval-auditor.md": "1b8391f1aafb067f",
"agents/gsd-eval-planner.md": "3d10fd11147f6857",
"agents/gsd-executor.md": "152955064cf4c9d2",
"agents/gsd-executor.md": "707f7f26bc2c1c59",
"agents/gsd-framework-selector.md": "daa62c79619c76bf",
"agents/gsd-integration-checker.md": "0643cd2d779b131c",
"agents/gsd-intel-updater.md": "26c1f1e028c6346a",
@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
"gsd-core/bin/gsd-tools.cjs": "2fcf88250beb9214",
"gsd-core/bin/gsd-tools.cjs": "8c5bf9788c3967be",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -289,7 +289,7 @@
"gsd-core/workflows/plant-seed.md": "6cf726be6427a35a",
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
"gsd-core/workflows/progress.md": "0e41db02a270a2b2",
"gsd-core/workflows/progress.md": "9fb8c93970e0b4aa",
"gsd-core/workflows/quick.md": "29739776f5c91338",
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
@@ -302,7 +302,7 @@
"gsd-core/workflows/settings-advanced.md": "6d25100a9de15b31",
"gsd-core/workflows/settings-integrations.md": "4d3001ad2b5dad8f",
"gsd-core/workflows/settings.md": "8258f7bd3700d608",
"gsd-core/workflows/ship.md": "8575dcc6d75a17c5",
"gsd-core/workflows/ship.md": "3fb340ea87726ed2",
"gsd-core/workflows/sketch-wrap-up.md": "0f842a609851a401",
"gsd-core/workflows/sketch.md": "314b7d323c6b57eb",
"gsd-core/workflows/smart-entry.md": "3ce5b6228238fdb6",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "671c9ea949889c4a",
"agents/gsd-eval-auditor.md": "fcaec7b00f94c435",
"agents/gsd-eval-planner.md": "a4a5b4b3f7828ba3",
"agents/gsd-executor.md": "47c648f6622e939f",
"agents/gsd-executor.md": "28fbb5fa6eb49c65",
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
"agents/gsd-integration-checker.md": "fa53e2d78be1de74",
"agents/gsd-intel-updater.md": "fa40e685d7441ace",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "14263db831230142",
"gsd-core/workflows/progress.md": "9457fb10437c9402",
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
"gsd-core/workflows/quick.md": "988fbe85a0251cf1",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
"gsd-core/workflows/ship.md": "369f1461685d1897",
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
"gsd-core/workflows/sketch-wrap-up.md": "5f5ebb6a80d610c6",
"gsd-core/workflows/sketch.md": "8319cedf3f93fc35",
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",

View File

@@ -15,7 +15,7 @@
"agents/gsd-domain-researcher.md": "f1e03df842ddfb95",
"agents/gsd-eval-auditor.md": "d0f45fff7370bb0b",
"agents/gsd-eval-planner.md": "9cc049b82897daa4",
"agents/gsd-executor.md": "3c8d4fd1c29542bb",
"agents/gsd-executor.md": "a140ad7b13e4af95",
"agents/gsd-framework-selector.md": "85005d716f9d98f7",
"agents/gsd-integration-checker.md": "17a8ee731986564d",
"agents/gsd-intel-updater.md": "4953a465db9dadc1",
@@ -109,7 +109,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -359,7 +359,7 @@
"gsd-core/workflows/plant-seed.md": "fbe964fcdb244802",
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
"gsd-core/workflows/progress.md": "f6739fc007de6f66",
"gsd-core/workflows/progress.md": "01079a85cbc41ffe",
"gsd-core/workflows/quick.md": "0f49d4553de5da88",
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
@@ -372,7 +372,7 @@
"gsd-core/workflows/settings-advanced.md": "94d61da368e9f85b",
"gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139",
"gsd-core/workflows/settings.md": "acdd79110699a608",
"gsd-core/workflows/ship.md": "cae3fbefba655818",
"gsd-core/workflows/ship.md": "30ff5e4ebf0edee8",
"gsd-core/workflows/sketch-wrap-up.md": "d52a5462bafda830",
"gsd-core/workflows/sketch.md": "dbe6acc4d976060c",
"gsd-core/workflows/smart-entry.md": "1850447c045f36d8",

View File

@@ -15,7 +15,7 @@
"agents/gsd-domain-researcher.md": "5f7d366251b957fe",
"agents/gsd-eval-auditor.md": "fea2759beff0a642",
"agents/gsd-eval-planner.md": "112f6730f23854e3",
"agents/gsd-executor.md": "e63187a0bbff74eb",
"agents/gsd-executor.md": "a3c5bd2940dcf7c6",
"agents/gsd-framework-selector.md": "c350ee693cb1aa4e",
"agents/gsd-integration-checker.md": "c8b4e65dee89c8ea",
"agents/gsd-intel-updater.md": "5b41e05f90ce89d9",
@@ -38,7 +38,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -288,7 +288,7 @@
"gsd-core/workflows/plant-seed.md": "af50e9f10d3cc6e1",
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
"gsd-core/workflows/progress.md": "8aa2fcebb8b4876e",
"gsd-core/workflows/progress.md": "1b56219777ea078c",
"gsd-core/workflows/quick.md": "46da35933571f3f6",
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
@@ -301,7 +301,7 @@
"gsd-core/workflows/settings-advanced.md": "339def28c34b0797",
"gsd-core/workflows/settings-integrations.md": "53649313d20694ae",
"gsd-core/workflows/settings.md": "7e7458cdb2b68ec5",
"gsd-core/workflows/ship.md": "f777449b79ee3357",
"gsd-core/workflows/ship.md": "cf9425876e736a52",
"gsd-core/workflows/sketch-wrap-up.md": "121ed4b8127abf04",
"gsd-core/workflows/sketch.md": "737c0492686fea2d",
"gsd-core/workflows/smart-entry.md": "ad20cf74ae2e8291",

View File

@@ -19,7 +19,7 @@
"agents/gsd-domain-researcher.md": "0fecdaea86466a56",
"agents/gsd-eval-auditor.md": "36c44303085df2f8",
"agents/gsd-eval-planner.md": "3ddea88a69b4da3f",
"agents/gsd-executor.md": "343f7d59b160d74a",
"agents/gsd-executor.md": "8443c52fc864d84e",
"agents/gsd-framework-selector.md": "564669d479433f15",
"agents/gsd-integration-checker.md": "1bbbdd3d420b994e",
"agents/gsd-intel-updater.md": "42c40fffbc720d0b",
@@ -42,7 +42,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf",
"gsd-core/bin/gsd-tools.cjs": "81330ef8b3bb63c1",
"gsd-core/bin/gsd-tools.cjs": "36d17f5566bdec9e",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -292,7 +292,7 @@
"gsd-core/workflows/plant-seed.md": "a2cdd513663226f1",
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
"gsd-core/workflows/progress.md": "a2d948b084a48500",
"gsd-core/workflows/progress.md": "53b4310b873d233e",
"gsd-core/workflows/quick.md": "f4d73a7a52b11bd1",
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
@@ -305,7 +305,7 @@
"gsd-core/workflows/settings-advanced.md": "69f3a19bf2c61160",
"gsd-core/workflows/settings-integrations.md": "76eee76d6eb57657",
"gsd-core/workflows/settings.md": "3701faed09d55247",
"gsd-core/workflows/ship.md": "a58d05ab03124d0b",
"gsd-core/workflows/ship.md": "e2eb0a000c515798",
"gsd-core/workflows/sketch-wrap-up.md": "1f44789553180d84",
"gsd-core/workflows/sketch.md": "2226779b6003a71c",
"gsd-core/workflows/smart-entry.md": "9a64f43927641ca4",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "1c1a800108a2b225",
"agents/gsd-eval-auditor.md": "99012004b14ea602",
"agents/gsd-eval-planner.md": "4ebdd7fe9cbb0cfe",
"agents/gsd-executor.md": "44149e405a126f90",
"agents/gsd-executor.md": "87ae9188c389bde8",
"agents/gsd-framework-selector.md": "7726fccc86bfeb50",
"agents/gsd-integration-checker.md": "2d8339790bbb2dc3",
"agents/gsd-intel-updater.md": "c51339956197cbd3",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
"gsd-core/workflows/progress.md": "9457fb10437c9402",
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
"gsd-core/workflows/quick.md": "68a60c8fe332cb4e",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
"gsd-core/workflows/ship.md": "369f1461685d1897",
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
"gsd-core/workflows/sketch-wrap-up.md": "2aba89ecd8f41a0d",
"gsd-core/workflows/sketch.md": "5eedd93f9a5b49d5",
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",

View File

@@ -102,8 +102,8 @@
"agents/gsd-eval-auditor.toml": "9b81d61b3c5f722d",
"agents/gsd-eval-planner.md": "73f2ad2ff2797a51",
"agents/gsd-eval-planner.toml": "09468ad1a34ac468",
"agents/gsd-executor.md": "ed8080096522f7ec",
"agents/gsd-executor.toml": "126d79d90e2fdf4f",
"agents/gsd-executor.md": "45442cc002be7052",
"agents/gsd-executor.toml": "bfb08e9397bcc358",
"agents/gsd-framework-selector.md": "ebae32430887d2e0",
"agents/gsd-framework-selector.toml": "637e4e021b7ec380",
"agents/gsd-integration-checker.md": "9cc875676cf7d741",
@@ -145,7 +145,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -395,7 +395,7 @@
"gsd-core/workflows/plant-seed.md": "5b07de07e4593281",
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
"gsd-core/workflows/progress.md": "03f12b223eb3a96e",
"gsd-core/workflows/progress.md": "e57d5a148df38508",
"gsd-core/workflows/quick.md": "ef0af6ee7788f64b",
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
@@ -408,7 +408,7 @@
"gsd-core/workflows/settings-advanced.md": "2431433811616f76",
"gsd-core/workflows/settings-integrations.md": "77730321d3d6d317",
"gsd-core/workflows/settings.md": "054c8c31b3905ced",
"gsd-core/workflows/ship.md": "2c2ab4e0243a2b27",
"gsd-core/workflows/ship.md": "ccd8cda30a4a4231",
"gsd-core/workflows/sketch-wrap-up.md": "07724a390fbb43f6",
"gsd-core/workflows/sketch.md": "576f300dfdde1b7d",
"gsd-core/workflows/smart-entry.md": "6f686195ae531b03",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.agent.md": "d603239b3e9fe428",
"agents/gsd-eval-auditor.agent.md": "3c03009564de55c8",
"agents/gsd-eval-planner.agent.md": "14751876fc2b5f16",
"agents/gsd-executor.agent.md": "e75e46a35b7b260a",
"agents/gsd-executor.agent.md": "bca7d1ab12392956",
"agents/gsd-framework-selector.agent.md": "cafeec0b3489be45",
"agents/gsd-integration-checker.agent.md": "30439b804927acc7",
"agents/gsd-intel-updater.agent.md": "238c1a886f35a25c",
@@ -40,7 +40,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
"gsd-core/bin/gsd-tools.cjs": "2fcf88250beb9214",
"gsd-core/bin/gsd-tools.cjs": "8c5bf9788c3967be",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -290,7 +290,7 @@
"gsd-core/workflows/plant-seed.md": "21e461cd39e5181b",
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
"gsd-core/workflows/progress.md": "449b7f4de1bef7ae",
"gsd-core/workflows/progress.md": "97f4971366c381ee",
"gsd-core/workflows/quick.md": "dff6d086ef16a14f",
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
@@ -303,7 +303,7 @@
"gsd-core/workflows/settings-advanced.md": "230a658de9c017a6",
"gsd-core/workflows/settings-integrations.md": "a1d146d6bfd14db5",
"gsd-core/workflows/settings.md": "f611f14f2f447f1e",
"gsd-core/workflows/ship.md": "7e52f4b11bd15b58",
"gsd-core/workflows/ship.md": "9a260c559e783769",
"gsd-core/workflows/sketch-wrap-up.md": "f2590cb6ddbfad94",
"gsd-core/workflows/sketch.md": "e2063966439af8c1",
"gsd-core/workflows/smart-entry.md": "6c707b959a41900b",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "56395dbdabf076f6",
"agents/gsd-eval-auditor.md": "ad2840fd5cd76172",
"agents/gsd-eval-planner.md": "2049dac060d00eda",
"agents/gsd-executor.md": "e3d78836e115b18c",
"agents/gsd-executor.md": "e568f8171c7d12f1",
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
"agents/gsd-integration-checker.md": "5da30584d06b878c",
"agents/gsd-intel-updater.md": "b8971c5d96e63b38",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828",
"gsd-core/bin/gsd-tools.cjs": "31f45cfd36e5dfdb",
"gsd-core/bin/gsd-tools.cjs": "40e8e83311b87500",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "7bccd151ce7b69f2",
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
"gsd-core/workflows/progress.md": "0f03aaf96f5abf7e",
"gsd-core/workflows/progress.md": "2dbc495deed1a36d",
"gsd-core/workflows/quick.md": "ad40466dd04e4147",
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "bf8ea69c8f7ae019",
"gsd-core/workflows/settings-integrations.md": "166ca51b1f33c2a3",
"gsd-core/workflows/settings.md": "0623c673eaf04799",
"gsd-core/workflows/ship.md": "6e3ac35dc933a39a",
"gsd-core/workflows/ship.md": "1515bb4909387b1d",
"gsd-core/workflows/sketch-wrap-up.md": "5be73b7bdf96b539",
"gsd-core/workflows/sketch.md": "373bc0d83368a411",
"gsd-core/workflows/smart-entry.md": "c8fc316358cdcd7b",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "412cdbb05ba252ea",
"agents/gsd-eval-auditor.md": "4ffb265063c318e5",
"agents/gsd-eval-planner.md": "03448fc9c5774b56",
"agents/gsd-executor.md": "b70c976372c77dc1",
"agents/gsd-executor.md": "e39b2cd4ec6b3afd",
"agents/gsd-framework-selector.md": "ea9981d65d6b3429",
"agents/gsd-integration-checker.md": "35b4f2969d279871",
"agents/gsd-intel-updater.md": "5fe5edfae2719cb8",
@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f",
"gsd-core/bin/gsd-tools.cjs": "6df1804284c68471",
"gsd-core/bin/gsd-tools.cjs": "c7e11161f0296129",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -289,7 +289,7 @@
"gsd-core/workflows/plant-seed.md": "f862f77fca983749",
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
"gsd-core/workflows/progress.md": "debe980226d9260d",
"gsd-core/workflows/progress.md": "589f60eb809c0bb9",
"gsd-core/workflows/quick.md": "fab58ce8581251fa",
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
@@ -302,7 +302,7 @@
"gsd-core/workflows/settings-advanced.md": "49be159144d7f426",
"gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5",
"gsd-core/workflows/settings.md": "9ced580679ed0255",
"gsd-core/workflows/ship.md": "8e8c1d3c8204769d",
"gsd-core/workflows/ship.md": "b2f16c18a7e1e19b",
"gsd-core/workflows/sketch-wrap-up.md": "f1ece50ac65ea281",
"gsd-core/workflows/sketch.md": "d5887983e62b574a",
"gsd-core/workflows/smart-entry.md": "47f5c5e8608e5f7a",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "a3874d80bcbc7380",
"agents/gsd-eval-auditor.md": "630d4cd3bd6ea195",
"agents/gsd-eval-planner.md": "3db12cde12aeb2c1",
"agents/gsd-executor.md": "73278ea652cac62f",
"agents/gsd-executor.md": "14c1681bb5f7214f",
"agents/gsd-framework-selector.md": "ad5f2c6b9bec6270",
"agents/gsd-integration-checker.md": "c503e2f4a3d8ec05",
"agents/gsd-intel-updater.md": "231393da62a45b2e",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "ffa5774304243649",
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
"gsd-core/workflows/progress.md": "c2a8a79af6603e70",
"gsd-core/workflows/progress.md": "172e4168e326ca61",
"gsd-core/workflows/quick.md": "4ef7da71a596604a",
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "edd858cd6cfddaf1",
"gsd-core/workflows/settings-integrations.md": "3ea8095d5fad891b",
"gsd-core/workflows/settings.md": "1925ecc2225c2216",
"gsd-core/workflows/ship.md": "7afe19fa2d472013",
"gsd-core/workflows/ship.md": "cb5f2f25ca80bd3b",
"gsd-core/workflows/sketch-wrap-up.md": "888c0548e63197b3",
"gsd-core/workflows/sketch.md": "34a0c10fa56af7ea",
"gsd-core/workflows/smart-entry.md": "7ffe4fdb93935400",

View File

@@ -61,7 +61,7 @@
"agents/subagents/gsd-eval-auditor.yaml": "e3d868bd5fefe938",
"agents/subagents/gsd-eval-planner.md": "70f8c5727bfb9876",
"agents/subagents/gsd-eval-planner.yaml": "df8499f7af297ec2",
"agents/subagents/gsd-executor.md": "20cf0b2aa886d35c",
"agents/subagents/gsd-executor.md": "e37a910127d9ca5c",
"agents/subagents/gsd-executor.yaml": "e29422986636fd64",
"agents/subagents/gsd-framework-selector.md": "a15b7aa1e0576e16",
"agents/subagents/gsd-framework-selector.yaml": "fb52c31cde27b0e3",
@@ -103,7 +103,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -353,7 +353,7 @@
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
"gsd-core/workflows/progress.md": "9457fb10437c9402",
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
"gsd-core/workflows/quick.md": "2a9476454b69b4ff",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
@@ -366,7 +366,7 @@
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
"gsd-core/workflows/ship.md": "369f1461685d1897",
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
"gsd-core/workflows/sketch-wrap-up.md": "b767a1d3db129a8a",
"gsd-core/workflows/sketch.md": "88cfdf4edcf222ab",
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "71250e759ca9e723",
"agents/gsd-eval-auditor.md": "c88890105f32ace6",
"agents/gsd-eval-planner.md": "60bddb70a937f796",
"agents/gsd-executor.md": "84c0a7dc19ddb6c5",
"agents/gsd-executor.md": "4946be74860ea68e",
"agents/gsd-framework-selector.md": "1c0a10355e787675",
"agents/gsd-integration-checker.md": "a9de5928e5a5c649",
"agents/gsd-intel-updater.md": "493e07482fa6198a",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "b8dad652e31c2318",
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
"gsd-core/workflows/progress.md": "352920a61ed4f4db",
"gsd-core/workflows/progress.md": "c3d7149133d571a9",
"gsd-core/workflows/quick.md": "f2641bad95c6614e",
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "252b0d3edc315339",
"gsd-core/workflows/settings-integrations.md": "d1711a95f44fdbde",
"gsd-core/workflows/settings.md": "2e42ee34c791378a",
"gsd-core/workflows/ship.md": "fb70add4f61cf3e8",
"gsd-core/workflows/ship.md": "9e773a94f4abf0e6",
"gsd-core/workflows/sketch-wrap-up.md": "681800323681c5c6",
"gsd-core/workflows/sketch.md": "fcb7af914159ef7b",
"gsd-core/workflows/smart-entry.md": "2a253fe437496eea",

View File

@@ -6,7 +6,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -256,7 +256,7 @@
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
"gsd-core/workflows/progress.md": "9457fb10437c9402",
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
"gsd-core/workflows/quick.md": "c00a6574ff650a86",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
@@ -269,7 +269,7 @@
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
"gsd-core/workflows/ship.md": "369f1461685d1897",
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
"gsd-core/workflows/sketch-wrap-up.md": "838c701bd072ae73",
"gsd-core/workflows/sketch.md": "c7725562b3efd311",
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "bd054bb27beed2a7",
"agents/gsd-eval-auditor.md": "57cc7458ab5de6b7",
"agents/gsd-eval-planner.md": "01b665728dde4ccf",
"agents/gsd-executor.md": "6dcccfef842a49ac",
"agents/gsd-executor.md": "a6dd5b7da329d8f2",
"agents/gsd-framework-selector.md": "82ba6abea84226b7",
"agents/gsd-integration-checker.md": "90835dbc7dfa1691",
"agents/gsd-intel-updater.md": "3cc4f6ddd04676ec",
@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2",
"gsd-core/bin/gsd-tools.cjs": "ebbae1afe8c7fcd5",
"gsd-core/bin/gsd-tools.cjs": "c67b43694423939a",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -289,7 +289,7 @@
"gsd-core/workflows/plant-seed.md": "0a92ba12993ac261",
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
"gsd-core/workflows/progress.md": "4ceb0f6f5e34c4a4",
"gsd-core/workflows/progress.md": "475eb60a2e739eed",
"gsd-core/workflows/quick.md": "f5de70665fbea4fa",
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
@@ -302,7 +302,7 @@
"gsd-core/workflows/settings-advanced.md": "5e05212fb5cba531",
"gsd-core/workflows/settings-integrations.md": "29c5de27fbbb18e9",
"gsd-core/workflows/settings.md": "26b9b7979d3a5747",
"gsd-core/workflows/ship.md": "55cc3a0b0d188021",
"gsd-core/workflows/ship.md": "15420ac2a46fc203",
"gsd-core/workflows/sketch-wrap-up.md": "89e0eab2af946b04",
"gsd-core/workflows/sketch.md": "483387542d6fc3af",
"gsd-core/workflows/smart-entry.md": "d309710bcabd4675",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "b80f76874c04e515",
"agents/gsd-eval-auditor.md": "470bf16303ec4d2e",
"agents/gsd-eval-planner.md": "22334fde85723c9d",
"agents/gsd-executor.md": "b3baa767abac0593",
"agents/gsd-executor.md": "a611d34bf51e6aec",
"agents/gsd-framework-selector.md": "7726fccc86bfeb50",
"agents/gsd-integration-checker.md": "7cd2072984411c7f",
"agents/gsd-intel-updater.md": "83de6ba9172891c3",
@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527",
"gsd-core/bin/gsd-tools.cjs": "1673458b3332be8a",
"gsd-core/bin/gsd-tools.cjs": "44e114ddf0f32e08",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -289,7 +289,7 @@
"gsd-core/workflows/plant-seed.md": "856ad565b2eb0c47",
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
"gsd-core/workflows/progress.md": "00de1afbe907efa5",
"gsd-core/workflows/progress.md": "1fe098b4d559ba55",
"gsd-core/workflows/quick.md": "95ad5630aca7f1e2",
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
@@ -302,7 +302,7 @@
"gsd-core/workflows/settings-advanced.md": "39e66386f6c48025",
"gsd-core/workflows/settings-integrations.md": "f8f756709ec02363",
"gsd-core/workflows/settings.md": "44b10c59215633b8",
"gsd-core/workflows/ship.md": "7d6e81541d393db2",
"gsd-core/workflows/ship.md": "58d9fd3ee13ce1c2",
"gsd-core/workflows/sketch-wrap-up.md": "dbec602d104cb951",
"gsd-core/workflows/sketch.md": "44ff275150b6d045",
"gsd-core/workflows/smart-entry.md": "d8018578571f08d5",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "56395dbdabf076f6",
"agents/gsd-eval-auditor.md": "fb64fc5acf359747",
"agents/gsd-eval-planner.md": "2049dac060d00eda",
"agents/gsd-executor.md": "45c4632bc67a12a2",
"agents/gsd-executor.md": "067a4f846f4e020c",
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
"agents/gsd-integration-checker.md": "4ffb37fb230c2b90",
"agents/gsd-intel-updater.md": "a81d77c143c02108",
@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2",
"gsd-core/bin/gsd-tools.cjs": "ecc544c0f939432b",
"gsd-core/bin/gsd-tools.cjs": "3224906065bfb20c",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -289,7 +289,7 @@
"gsd-core/workflows/plant-seed.md": "6cf61f540cdbd8b9",
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
"gsd-core/workflows/progress.md": "7b6b9564b34fcabb",
"gsd-core/workflows/progress.md": "3e4ac518362ba665",
"gsd-core/workflows/quick.md": "baff92603ade7489",
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
@@ -302,7 +302,7 @@
"gsd-core/workflows/settings-advanced.md": "2f86ec7b998f9485",
"gsd-core/workflows/settings-integrations.md": "b082fc518b484c07",
"gsd-core/workflows/settings.md": "002eb0ce3c10c741",
"gsd-core/workflows/ship.md": "6f611c07a1d34ff2",
"gsd-core/workflows/ship.md": "63ea5e85cbd33058",
"gsd-core/workflows/sketch-wrap-up.md": "10063f56c2c7f141",
"gsd-core/workflows/sketch.md": "25c1f8f7acfb1da9",
"gsd-core/workflows/smart-entry.md": "bd81482cb6a7ac53",

View File

@@ -16,7 +16,7 @@
"agents/gsd-domain-researcher.md": "049f588663814fa2",
"agents/gsd-eval-auditor.md": "54870d3b07433525",
"agents/gsd-eval-planner.md": "552e9fa164c51ce8",
"agents/gsd-executor.md": "59fbb1e047b9a7c4",
"agents/gsd-executor.md": "1154cdd7b22f9754",
"agents/gsd-framework-selector.md": "8a795f230436ad2e",
"agents/gsd-integration-checker.md": "c1760a0bbd4f7bf5",
"agents/gsd-intel-updater.md": "944f1d903e2e9e09",
@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
@@ -360,7 +360,7 @@
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
"gsd-core/workflows/progress.md": "9457fb10437c9402",
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
"gsd-core/workflows/quick.md": "5331f238c75a821f",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
@@ -373,7 +373,7 @@
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
"gsd-core/workflows/ship.md": "369f1461685d1897",
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
"gsd-core/workflows/sketch-wrap-up.md": "86db87b16548117e",
"gsd-core/workflows/sketch.md": "e96f1866d3e60cab",
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",

View File

@@ -291,19 +291,27 @@ describe('frontmatter get SECURITY.md threats_open — type contract', () => {
describe('real registry ship:pre — structural guards', () => {
test('ship:pre byLoopPoint entry has exactly 1 gate and 0 steps/contributions', () => {
test('ship:pre byLoopPoint entry has only gates (no steps/contributions) and includes security + broken-windows', () => {
const entry = realRegistry.byLoopPoint['ship:pre'];
assert.ok(entry, 'ship:pre must exist in byLoopPoint');
assert.strictEqual(entry.steps.length, 0, 'ship:pre must have 0 steps');
assert.strictEqual(entry.contributions.length, 0, 'ship:pre must have 0 contributions');
assert.strictEqual(entry.gates.length, 1, 'ship:pre must have exactly 1 gate (security)');
// Two predicate-style gates as of #1950: security (workflow.security_enforcement)
// and broken-windows (workflow.windows_enforce). Both default-off in tests via
// their respective when keys; both surface in the registry regardless of activation.
assert.strictEqual(entry.gates.length, 2, 'ship:pre must have exactly 2 gates (security + broken-windows)');
const capIds = entry.gates.map(g => g.capId).sort();
assert.deepEqual(capIds, ['broken-windows', 'security'], 'ship:pre gate capIds must be {security, broken-windows}');
});
test('ship:pre gate capId is "security" and check has predicate not query', () => {
const gate = realRegistry.byLoopPoint['ship:pre'].gates[0];
assert.strictEqual(gate.capId, 'security');
assert.ok(gate.check.predicate, 'ship:pre gate must use predicate, not query');
assert.strictEqual(gate.check.query, undefined, 'ship:pre must NOT have a check.query (predicate-only gate)');
test('every ship:pre gate uses predicate (not query) and the security gate is present', () => {
const gates = realRegistry.byLoopPoint['ship:pre'].gates;
for (const gate of gates) {
assert.ok(gate.check.predicate, `ship:pre gate ${gate.capId} must use predicate, not query`);
assert.strictEqual(gate.check.query, undefined, `ship:pre gate ${gate.capId} must NOT have a check.query (predicate-only gate)`);
}
const security = gates.find(g => g.capId === 'security');
assert.ok(security, 'ship:pre must include the security gate');
});
});

View File

@@ -58,7 +58,7 @@
"plant-seed.md": 11785,
"pr-branch.md": 15963,
"profile-user.md": 21246,
"progress.md": 31789,
"progress.md": 32632,
"quick.md": 50993,
"reapply-patches.md": 20312,
"remove-phase.md": 8513,
@@ -71,7 +71,7 @@
"settings-advanced.md": 40019,
"settings-integrations.md": 15892,
"settings.md": 33467,
"ship.md": 25575,
"ship.md": 27945,
"sketch-wrap-up.md": 14267,
"sketch.md": 20004,
"smart-entry.md": 11124,