cyber-sdd 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.claude-plugin/plugin.json +17 -0
  2. package/.codex-plugin/plugin.json +17 -0
  3. package/.plugin/plugin.json +17 -0
  4. package/README.md +159 -0
  5. package/agents/sdd-automaton.md +97 -0
  6. package/agents/sdd-impl-judge.md +214 -0
  7. package/agents/sdd-scanner.md +120 -0
  8. package/agents/sdd-spec-judge.md +224 -0
  9. package/agents/sdd-warden.md +101 -0
  10. package/package.json +24 -0
  11. package/skills/align-spec/README.md +20 -0
  12. package/skills/align-spec/SKILL.md +111 -0
  13. package/skills/align-spec/scripts/align-spec.mts +187 -0
  14. package/skills/architect-impl-governance/README.md +46 -0
  15. package/skills/architect-impl-governance/SKILL.md +45 -0
  16. package/skills/architect-spec-governance/README.md +48 -0
  17. package/skills/architect-spec-governance/SKILL.md +59 -0
  18. package/skills/blast-estimate/README.md +47 -0
  19. package/skills/blast-estimate/SKILL.md +133 -0
  20. package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
  21. package/skills/builder-impl-governance/README.md +47 -0
  22. package/skills/builder-impl-governance/SKILL.md +47 -0
  23. package/skills/builder-spec-governance/README.md +49 -0
  24. package/skills/builder-spec-governance/SKILL.md +36 -0
  25. package/skills/check-partition-quality/README.md +22 -0
  26. package/skills/check-partition-quality/SKILL.md +51 -0
  27. package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
  28. package/skills/check-plan-safety/README.md +17 -0
  29. package/skills/check-plan-safety/SKILL.md +60 -0
  30. package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
  31. package/skills/check-project-specs/README.md +19 -0
  32. package/skills/check-project-specs/SKILL.md +69 -0
  33. package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
  34. package/skills/check-scenario-overlap/README.md +19 -0
  35. package/skills/check-scenario-overlap/SKILL.md +74 -0
  36. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
  37. package/skills/check-spec-structure/README.md +17 -0
  38. package/skills/check-spec-structure/SKILL.md +66 -0
  39. package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
  40. package/skills/collision-ladder/README.md +18 -0
  41. package/skills/collision-ladder/SKILL.md +83 -0
  42. package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
  43. package/skills/combat-log-governance/README.md +13 -0
  44. package/skills/combat-log-governance/SKILL.md +257 -0
  45. package/skills/concept-index/README.md +13 -0
  46. package/skills/concept-index/SKILL.md +38 -0
  47. package/skills/concept-index/scripts/concept-index.mts +245 -0
  48. package/skills/discover-plans/README.md +16 -0
  49. package/skills/discover-plans/SKILL.md +74 -0
  50. package/skills/discover-plans/scripts/discover-plans.mts +212 -0
  51. package/skills/discover-specs/README.md +15 -0
  52. package/skills/discover-specs/SKILL.md +76 -0
  53. package/skills/discover-specs/scripts/discover-specs.mts +396 -0
  54. package/skills/doctrine-loop/README.md +15 -0
  55. package/skills/doctrine-loop/SKILL.md +97 -0
  56. package/skills/formation-loop/README.md +17 -0
  57. package/skills/formation-loop/SKILL.md +140 -0
  58. package/skills/gate-validation-governance/README.md +12 -0
  59. package/skills/gate-validation-governance/SKILL.md +87 -0
  60. package/skills/impl-producer-governance/README.md +48 -0
  61. package/skills/impl-producer-governance/SKILL.md +85 -0
  62. package/skills/init/README.md +27 -0
  63. package/skills/init/SKILL.md +68 -0
  64. package/skills/init/scripts/wire-statusline.mts +276 -0
  65. package/skills/lifecycle-governance/README.md +11 -0
  66. package/skills/lifecycle-governance/SKILL.md +168 -0
  67. package/skills/manage/README.md +9 -0
  68. package/skills/manage/SKILL.md +62 -0
  69. package/skills/manage-ignore/README.md +19 -0
  70. package/skills/manage-ignore/SKILL.md +52 -0
  71. package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
  72. package/skills/manage-scenario-bridge/README.md +20 -0
  73. package/skills/manage-scenario-bridge/SKILL.md +60 -0
  74. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
  75. package/skills/manage-spec-anchors/README.md +18 -0
  76. package/skills/manage-spec-anchors/SKILL.md +56 -0
  77. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
  78. package/skills/mission-graph/README.md +15 -0
  79. package/skills/mission-graph/SKILL.md +67 -0
  80. package/skills/mission-graph/scripts/mission-graph.mts +844 -0
  81. package/skills/oracle-spec-governance/README.md +45 -0
  82. package/skills/oracle-spec-governance/SKILL.md +45 -0
  83. package/skills/ownership-governance/README.md +65 -0
  84. package/skills/ownership-governance/SKILL.md +104 -0
  85. package/skills/pause-mission/README.md +18 -0
  86. package/skills/pause-mission/SKILL.md +112 -0
  87. package/skills/place-node/README.md +12 -0
  88. package/skills/place-node/SKILL.md +47 -0
  89. package/skills/place-node/scripts/place-node.mts +157 -0
  90. package/skills/plan-retirement/README.md +32 -0
  91. package/skills/plan-retirement/SKILL.md +90 -0
  92. package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
  93. package/skills/plugin-contract-governance/README.md +12 -0
  94. package/skills/plugin-contract-governance/SKILL.md +112 -0
  95. package/skills/remediation-governance/README.md +46 -0
  96. package/skills/remediation-governance/SKILL.md +78 -0
  97. package/skills/resolve-governances/README.md +18 -0
  98. package/skills/resolve-governances/SKILL.md +50 -0
  99. package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
  100. package/skills/resolve-tracking/SKILL.md +64 -0
  101. package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
  102. package/skills/resume-mission/README.md +12 -0
  103. package/skills/resume-mission/SKILL.md +53 -0
  104. package/skills/scaffold-project-spec/README.md +7 -0
  105. package/skills/scaffold-project-spec/SKILL.md +192 -0
  106. package/skills/sdd/README.md +7 -0
  107. package/skills/sdd/SKILL.md +92 -0
  108. package/skills/solution-producer-governance/README.md +9 -0
  109. package/skills/solution-producer-governance/SKILL.md +44 -0
  110. package/skills/spec-format-governance/README.md +73 -0
  111. package/skills/spec-format-governance/SKILL.md +114 -0
  112. package/skills/spec-gate/README.md +26 -0
  113. package/skills/spec-gate/SKILL.md +201 -0
  114. package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
  115. package/skills/spec-gate/scripts/check-suite.mts +501 -0
  116. package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
  117. package/skills/spec-producer-governance/README.md +7 -0
  118. package/skills/spec-producer-governance/SKILL.md +86 -0
  119. package/skills/spec-structure-governance/README.md +40 -0
  120. package/skills/spec-structure-governance/SKILL.md +169 -0
  121. package/skills/ssa-lowering/README.md +26 -0
  122. package/skills/ssa-lowering/SKILL.md +181 -0
  123. package/skills/start-mission/README.md +7 -0
  124. package/skills/start-mission/SKILL.md +115 -0
  125. package/skills/suite-format-governance/README.md +75 -0
  126. package/skills/suite-format-governance/SKILL.md +299 -0
  127. package/skills/suite-format-governance/references/rubric.md +313 -0
  128. package/skills/touch-set-correction/README.md +16 -0
  129. package/skills/touch-set-correction/SKILL.md +67 -0
  130. package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
  131. package/skills/verify-scenarios/README.md +17 -0
  132. package/skills/verify-scenarios/SKILL.md +109 -0
  133. package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ // check-plan-safety — the concrete guard engine for the plan brief's safe-to-publish floor.
3
+ // Scans the SDD plan directory (.agents/plans) for the *.md handoff artifacts a mission commits
4
+ // — the `<cr-ref>.plan.md` brief and its sibling design docs — and flags any machine-local
5
+ // reference that must never enter git history in a tracked, portable artifact:
6
+ //
7
+ // - a home-directory absolute path (`/home/<user>/…`, `/Users/<user>/…`, `C:\Users\<user>\…`)
8
+ // — carries the OS username (privacy) and resolves on no other checkout (portability), and
9
+ // - a shell expansion of the user's home / identity (`$HOME`, `${HOME}`, `$USER`, `%USERPROFILE%`).
10
+ //
11
+ // A bare `~/` is deliberately NOT flagged: it carries no username and legitimately appears in
12
+ // design prose describing home-rooted feature paths (e.g. a tool's own `~/.<tool>/` data root).
13
+ // The floor mirrors the combat-log's "never committed: … absolute paths, OS usernames" rule
14
+ // (combat-log-governance), extended from the ledger to the plan brief.
15
+ //
16
+ // Pure functions are exported for node:test; running the file directly drives the CLI. No
17
+ // dependencies (the repo's node-≥23.6 / no-deps convention). Read-only: it writes nothing.
18
+ // Default output is TOON (the token-efficient tabular form); --format json for a flat array.
19
+ // --check is the CI guard: exit non-zero iff any leak is found.
20
+
21
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+
24
+ export interface Leak {
25
+ /** Repo-relative path of the file the leak sits in. */
26
+ file: string
27
+ /** 1-indexed line number. */
28
+ line: number
29
+ /** The leak class — `home-abs-path`, `env-home`, or `env-user`. */
30
+ kind: string
31
+ /** The matched fragment (capped), so a reader can locate and scrub it. */
32
+ token: string
33
+ }
34
+
35
+ // The machine-local reference patterns. Each carries the leak `kind` it detects. Global flag so a
36
+ // single line with several leaks reports each. USER/HOME env forms use a negative lookahead so
37
+ // `$HOMEBREW` / `$USERDATA` (a different variable that merely starts with the name) is not flagged.
38
+ export const LEAK_PATTERNS: { kind: string; re: RegExp }[] = [
39
+ // /home/<user>/… and /Users/<user>/… — the home-abs path the leak class is named for.
40
+ // Also catches a forward-slash Windows profile (`C:/Users/…` contains `/Users/…`).
41
+ { kind: 'home-abs-path', re: /\/(?:home|Users)\/[^\s'"`)\]]+/g },
42
+ // Backslash Windows user profile — C:\Users\<user>\… (the forward-slash form is caught above).
43
+ { kind: 'home-abs-path', re: /[A-Za-z]:\\Users\\[^\s'"`)\]]+/g },
44
+ { kind: 'env-home', re: /\$\{?HOME\}?(?![A-Za-z])|%USERPROFILE%|%HOMEPATH%/g },
45
+ { kind: 'env-user', re: /\$\{?USER\}?(?![A-Za-z])|%USERNAME%/g },
46
+ ]
47
+
48
+ /** Cap a matched token so a pathological long path does not blow up the report. */
49
+ function capToken(t: string): string {
50
+ return t.length > 80 ? `${t.slice(0, 77)}...` : t
51
+ }
52
+
53
+ // ── Scan one file's text ──
54
+ // Every machine-local reference in `text`, tagged with `file` (the repo-relative path the caller
55
+ // supplies) and its 1-indexed line. Lines are scanned independently; a line with N leaks yields N.
56
+ export function scanText(file: string, text: string): Leak[] {
57
+ const out: Leak[] = []
58
+ const lines = text.split('\n')
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i].replace(/\r$/, '')
61
+ for (const { kind, re } of LEAK_PATTERNS) {
62
+ re.lastIndex = 0
63
+ let m = re.exec(line)
64
+ while (m !== null) {
65
+ out.push({ file, line: i + 1, kind, token: capToken(m[0]) })
66
+ if (m.index === re.lastIndex) re.lastIndex++ // guard against a zero-width match
67
+ m = re.exec(line)
68
+ }
69
+ }
70
+ }
71
+ return out
72
+ }
73
+
74
+ // ── Scan the plan directory ──
75
+ // The handoff artifacts a mission commits: every `*.md` sitting directly under <root>/.agents/plans
76
+ // — the `<cr-ref>.plan.md` brief plus any sibling design doc. A missing plans dir yields no leaks.
77
+ function planFiles(root: string): string[] {
78
+ const dir = join(root, '.agents', 'plans')
79
+ if (!existsSync(dir)) return []
80
+ let entries: import('node:fs').Dirent[]
81
+ try {
82
+ entries = readdirSync(dir, { withFileTypes: true })
83
+ } catch {
84
+ return []
85
+ }
86
+ return entries
87
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
88
+ .map((e) => e.name)
89
+ .sort()
90
+ }
91
+
92
+ // Collect leaks across the plan directory (default) or a caller-named file set (`--path`). File
93
+ // paths in the returned leaks are repo-relative when scanning the plan dir, and as-passed for
94
+ // explicit `--path` targets. Sorted by file then line for stable output.
95
+ export function collectLeaks(root: string, paths?: string[]): Leak[] {
96
+ const targets: { rel: string; abs: string }[] = paths
97
+ ? paths.map((p) => ({ rel: p, abs: p }))
98
+ : planFiles(root).map((name) => ({
99
+ rel: join('.agents', 'plans', name),
100
+ abs: join(root, '.agents', 'plans', name),
101
+ }))
102
+ const out: Leak[] = []
103
+ for (const { rel, abs } of targets) {
104
+ let text: string
105
+ try {
106
+ text = readFileSync(abs, 'utf8')
107
+ } catch {
108
+ continue
109
+ }
110
+ out.push(...scanText(rel, text))
111
+ }
112
+ return out.sort((a, b) => (a.file !== b.file ? (a.file < b.file ? -1 : 1) : a.line - b.line))
113
+ }
114
+
115
+ // ── Output ──
116
+ const COLUMNS = ['file', 'line', 'kind', 'token'] as const
117
+
118
+ function toonField(v: string): string {
119
+ if (v === '' || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
120
+ return v
121
+ }
122
+
123
+ export function toToon(leaks: Leak[]): string {
124
+ const header = `leaks[${leaks.length}]{${COLUMNS.join(',')}}:`
125
+ const rows = leaks.map((l) => ` ${COLUMNS.map((c) => toonField(String(l[c]))).join(',')}`)
126
+ return [header, ...rows].join('\n')
127
+ }
128
+
129
+ export function main(argv: string[]): number {
130
+ const root = argv.includes('--root') ? (argv[argv.indexOf('--root') + 1] ?? '.') : '.'
131
+ const format = argv.includes('--format') ? argv[argv.indexOf('--format') + 1] : 'toon'
132
+ const check = argv.includes('--check')
133
+ // `--path <file>` may repeat to scan an explicit file set (e.g. the one brief a checkpoint
134
+ // is about to commit) instead of the whole plan directory.
135
+ const paths: string[] = []
136
+ for (let i = 0; i < argv.length; i++) if (argv[i] === '--path' && argv[i + 1]) paths.push(argv[++i])
137
+
138
+ const leaks = collectLeaks(root, paths.length ? paths : undefined)
139
+ const out = format === 'json' ? JSON.stringify(leaks, null, 2) : toToon(leaks)
140
+ process.stdout.write(`${out}\n`)
141
+ // --check is the CI guard: any leak fails. Audit mode (default) always exits 0.
142
+ return check && leaks.length > 0 ? 1 : 0
143
+ }
144
+
145
+ if (import.meta.main) process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,19 @@
1
+ # check-project-specs
2
+
3
+ Runs every project-spec check against the one spec governing the invoking package.
4
+
5
+ ```bash
6
+ node scripts/check-project-specs.mts # resolve from cwd
7
+ node scripts/check-project-specs.mts --project <dir> # resolve an explicit project dir
8
+ ```
9
+
10
+ Wired as each project's `check:spec` script through the `sdd-check-specs` bin, so every project —
11
+ `plugins/*` and `packages/*` alike — runs the identical, path-free command.
12
+
13
+ Resolution is spec-first: the spec's own `project-path` names the project dir, and
14
+ `check-project-specs` inverts that map. The reverse map cannot be derived by name
15
+ (`plugins/cyberfleet` → `.agents/specs/cyberfleet-plugin`).
16
+
17
+ A project no spec governs prints a skip and exits zero. Two specs claiming one project is an error.
18
+
19
+ See `SKILL.md` for the engine set and the cwd contract.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: check-project-specs
3
+ description: "Partial Skill: invoke by name only — project-spec/check-project-specs' engine that runs every project-spec check against the one spec governing the invoking package — the per-project CI entrypoint, not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Check Project Specs
10
+
11
+ The **per-project entrypoint** for the project-spec checks. It resolves the one spec that governs
12
+ the invoking package and runs each project-spec engine against it, so a project's spec checks are a
13
+ task **the project owns** rather than a path some root script hardcodes. It carries a self-contained
14
+ `.mts` script (the repo's node-≥23.6 / no-deps convention).
15
+
16
+ ## Resolution — spec-first, never by name
17
+
18
+ A package knows its own directory; exactly one spec declares `project-path` pointing at it. The
19
+ engine inverts that map via `discover-specs`' `collectSpecs`:
20
+
21
+ 1. The project dir is the **cwd** (`--project <dir>` overrides — the cwd is what a package-manager
22
+ script gives you for free).
23
+ 2. Walk up for `pnpm-workspace.yaml` → the repo root.
24
+ 3. Match the repo-relative project dir against each spec's `project-path`.
25
+
26
+ The reverse map is **irregular and not derivable by name** — `plugins/cyberfleet` is governed by
27
+ `.agents/specs/cyberfleet-plugin`, and two different projects both own a skill named `init`. Only
28
+ `project-path` inverts reliably, which is why the spec stays the single source of truth for the
29
+ mapping and no path is ever written into a package's scripts.
30
+
31
+ ## Run it
32
+
33
+ ```bash
34
+ node "<skill>/scripts/check-project-specs.mts" [--project <dir>]
35
+ ```
36
+
37
+ Wired as each project's `check:spec` script, via the `sdd-check-specs` bin.
38
+
39
+ ## Outcomes
40
+
41
+ - **Resolved** — runs every engine against the spec dir, reports `ok` / `FAIL` per engine, and exits
42
+ non-zero if any failed.
43
+ - **No spec governs this project** — prints that and exits **zero**. The script is uniform across
44
+ every workspace member, and some members are governed by no spec; a project without one is not a
45
+ failure.
46
+ - **Two specs claim the project** — exits non-zero. One project is one spec.
47
+
48
+ ## The engines it runs
49
+
50
+ `check-spec-state` and `check-suite` (each `--root <specDir>`), then `concept-index`,
51
+ `check-spec-structure`, and `align-spec` (each `--spec-dir <specDir> --check`).
52
+
53
+ **`check-scenario-overlap` is not in this set yet.** Per-project it reports pre-existing
54
+ exact-duplicate scenarios that are `@trigger` sibling-deference rows; resolving one deletes a frozen
55
+ scenario from its non-owning node, which is a **narrowing** and Clearance-bound — not a call this
56
+ engine may force. It still runs corpus-wide at the root, so no coverage is lost, and it joins this
57
+ set in the CR that resolves those duplicates under a granted clearance.
58
+
59
+ Every engine is spawned with **cwd = the repo root**, never the project dir — they resolve
60
+ repo-root-relative references against the cwd.
61
+
62
+ The two `--root` engines are corpus-shaped (they read the first path segment under root as a project
63
+ slug), but a single project-spec dir is a legal root: the slug is only a message tag.
64
+
65
+ ## Boundaries
66
+
67
+ It owns no checks of its own — it resolves and delegates. It writes nothing, and it never decides
68
+ what a finding means. Adding a project-spec engine means adding it here, which is what keeps every
69
+ project's coverage identical.
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ // Runs every project-spec engine against the ONE project spec that governs the
3
+ // invoking package — resolved from the spec's own `project-path`, never from a
4
+ // hardcoded path. Pure functions are exported for node:test; running the file
5
+ // directly drives the CLI. No dependencies — plain node strips the types.
6
+ //
7
+ // The resolution is deliberately spec-first: a package knows its own directory,
8
+ // and exactly one spec declares `project-path` pointing at it. The reverse map
9
+ // (project dir -> spec dir) is irregular and cannot be derived by name —
10
+ // `plugins/cyberfleet` is governed by `.agents/specs/cyberfleet-plugin`.
11
+
12
+ import { execFileSync } from 'node:child_process'
13
+ import { existsSync, readFileSync } from 'node:fs'
14
+ import { dirname, join, relative, resolve } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { collectSpecs, discoverSpecFiles, type SpecRecord } from '../../discover-specs/scripts/discover-specs.mts'
17
+
18
+ const SKILLS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
19
+
20
+ // ─── the engine set ───────────────────────────────────────────────────────────
21
+
22
+ // Each engine is handed the resolved spec dir. The two `--root` engines are
23
+ // corpus-shaped (they treat the first path segment under root as a project
24
+ // slug) but accept a single project-spec dir as root — the slug is only a
25
+ // message tag, so a one-project root reports paths relative to that project.
26
+ interface Engine {
27
+ name: string
28
+ script: string
29
+ args: (specDir: string) => string[]
30
+ }
31
+
32
+ export const ENGINES: Engine[] = [
33
+ { name: 'check-spec-state', script: 'spec-gate/scripts/check-spec-state.mts', args: (d) => ['--root', d] },
34
+ { name: 'check-suite', script: 'spec-gate/scripts/check-suite.mts', args: (d) => ['--root', d] },
35
+ {
36
+ name: 'concept-index',
37
+ script: 'concept-index/scripts/concept-index.mts',
38
+ args: (d) => ['--spec-dir', d, '--check'],
39
+ },
40
+ {
41
+ name: 'check-spec-structure',
42
+ script: 'check-spec-structure/scripts/check-spec-structure.mts',
43
+ args: (d) => ['--spec-dir', d, '--check'],
44
+ },
45
+ { name: 'align-spec', script: 'align-spec/scripts/align-spec.mts', args: (d) => ['--spec-dir', d, '--check'] },
46
+ {
47
+ name: 'check-scenario-overlap',
48
+ script: 'check-scenario-overlap/scripts/check-scenario-overlap.mts',
49
+ args: (d) => ['--spec-dir', d, '--check'],
50
+ },
51
+ ]
52
+
53
+ // ─── repo root ────────────────────────────────────────────────────────────────
54
+
55
+ /** Walk up from `start` for the workspace marker. Returns '' when not found. */
56
+ export function findRepoRoot(start: string): string {
57
+ let dir = resolve(start)
58
+ for (;;) {
59
+ if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir
60
+ const up = dirname(dir)
61
+ if (up === dir) return ''
62
+ dir = up
63
+ }
64
+ }
65
+
66
+ // ─── resolution ───────────────────────────────────────────────────────────────
67
+
68
+ export type Resolution =
69
+ | { kind: 'resolved'; spec: SpecRecord }
70
+ | { kind: 'none' }
71
+ | { kind: 'ambiguous'; specs: SpecRecord[] }
72
+
73
+ /**
74
+ * Find the spec whose `project-path` names `projectRel` (a repo-relative dir).
75
+ * A project with no spec is `none` — not an error: the script is uniform across
76
+ * every workspace member, and some members are governed by no spec.
77
+ */
78
+ export function resolveSpecFor(specs: SpecRecord[], projectRel: string): Resolution {
79
+ const hits = specs.filter((s) => s.projectPath !== '' && s.projectPath === projectRel)
80
+ if (hits.length === 0) return { kind: 'none' }
81
+ if (hits.length > 1) return { kind: 'ambiguous', specs: hits }
82
+ return { kind: 'resolved', spec: hits[0] as SpecRecord }
83
+ }
84
+
85
+ // ─── coverage ─────────────────────────────────────────────────────────────────
86
+
87
+ export interface CoverageGap {
88
+ spec: string
89
+ projectPath: string
90
+ reason: 'unrecognized' | 'no-project-path' | 'no-manifest' | 'no-check-script'
91
+ }
92
+
93
+ /**
94
+ * Every spec must be reachable from a project that actually checks it. Without
95
+ * this, a spec silently goes unchecked the moment its project drops the script —
96
+ * which is exactly how the corpus ended up with one audited project out of ten.
97
+ *
98
+ * `specFiles` are the spec.md paths found at the recognized locations *before*
99
+ * the lifecycle-status filter; `specs` are the ones that survived it. A file in
100
+ * the first set and not the second is a spec whose status is not in the enum:
101
+ * discovery drops it, so every engine silently skips it and it is checked by
102
+ * nothing. That is a gap to escalate, not to exempt.
103
+ */
104
+ export function findCoverageGaps(
105
+ root: string,
106
+ specFiles: string[],
107
+ specs: SpecRecord[],
108
+ readPkg: (p: string) => unknown,
109
+ ): CoverageGap[] {
110
+ const gaps: CoverageGap[] = []
111
+ const recognized = new Set(specs.map((s) => (s.path === '' ? 'spec.md' : `${s.path}/spec.md`)))
112
+ for (const f of specFiles) {
113
+ if (!recognized.has(f)) gaps.push({ spec: f, projectPath: '', reason: 'unrecognized' })
114
+ }
115
+ for (const s of specs) {
116
+ if (s.projectPath === '') {
117
+ gaps.push({ spec: s.path, projectPath: '', reason: 'no-project-path' })
118
+ continue
119
+ }
120
+ const pkg = readPkg(join(root, s.projectPath, 'package.json')) as { scripts?: Record<string, string> } | null
121
+ if (!pkg) {
122
+ gaps.push({ spec: s.path, projectPath: s.projectPath, reason: 'no-manifest' })
123
+ continue
124
+ }
125
+ if (!pkg.scripts?.['check:spec']) {
126
+ gaps.push({ spec: s.path, projectPath: s.projectPath, reason: 'no-check-script' })
127
+ }
128
+ }
129
+ return gaps
130
+ }
131
+
132
+ const REASON_TEXT: Record<CoverageGap['reason'], string> = {
133
+ unrecognized:
134
+ 'sits at a spec location but its status is not in the lifecycle enum, so discovery drops it and nothing checks it',
135
+ 'no-project-path': 'declares no project-path, so no project can be resolved to check it',
136
+ 'no-manifest': 'names a project with no package.json, so it is not a workspace member',
137
+ 'no-check-script': 'names a project that defines no `check:spec` script',
138
+ }
139
+
140
+ function checkCoverage(root: string): number {
141
+ const gaps = findCoverageGaps(root, discoverSpecFiles(root), collectSpecs(root), (p) => {
142
+ try {
143
+ return JSON.parse(readFileSync(p, 'utf8'))
144
+ } catch {
145
+ return null
146
+ }
147
+ })
148
+ if (gaps.length === 0) {
149
+ process.stdout.write('check-project-specs: every spec is checked by its project\n')
150
+ return 0
151
+ }
152
+ for (const g of gaps) process.stderr.write(` ${g.spec} — ${REASON_TEXT[g.reason]}\n`)
153
+ process.stderr.write(`check-project-specs: ${gaps.length} spec(s) no project checks\n`)
154
+ return 1
155
+ }
156
+
157
+ // ─── run ──────────────────────────────────────────────────────────────────────
158
+
159
+ export function main(argv: string[]): number {
160
+ if (argv.includes('--check-coverage')) {
161
+ const root = findRepoRoot(process.cwd())
162
+ if (!root) {
163
+ process.stderr.write('check-project-specs: no pnpm-workspace.yaml found above the cwd\n')
164
+ return 1
165
+ }
166
+ return checkCoverage(root)
167
+ }
168
+ return checkProject(argv)
169
+ }
170
+
171
+ function checkProject(argv: string[]): number {
172
+ const projectArg = argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : undefined
173
+ const projectDir = resolve(projectArg ?? process.cwd())
174
+
175
+ const repoRoot = findRepoRoot(projectDir)
176
+ if (!repoRoot) {
177
+ process.stderr.write(`check-project-specs: no pnpm-workspace.yaml found above ${projectDir}\n`)
178
+ return 1
179
+ }
180
+
181
+ const projectRel = relative(repoRoot, projectDir) || '.'
182
+ const res = resolveSpecFor(collectSpecs(repoRoot), projectRel)
183
+
184
+ if (res.kind === 'none') {
185
+ process.stdout.write(`check-project-specs: no spec governs \`${projectRel}\` — skipped\n`)
186
+ return 0
187
+ }
188
+ if (res.kind === 'ambiguous') {
189
+ process.stderr.write(
190
+ `check-project-specs: \`${projectRel}\` is claimed by ${res.specs.length} specs ` +
191
+ `(${res.specs.map((s) => s.path).join(', ')}) — a project has exactly one spec\n`,
192
+ )
193
+ return 1
194
+ }
195
+
196
+ const specDir = join(repoRoot, res.spec.path)
197
+ process.stdout.write(`check-project-specs: ${projectRel} -> ${res.spec.path}\n`)
198
+
199
+ let failed = 0
200
+ for (const e of ENGINES) {
201
+ // cwd is the repo root, not the project dir: the engines resolve
202
+ // repo-root-relative references against process.cwd().
203
+ try {
204
+ execFileSync('node', [join(SKILLS_DIR, e.script), ...e.args(specDir)], {
205
+ cwd: repoRoot,
206
+ stdio: 'inherit',
207
+ })
208
+ process.stdout.write(` ok ${e.name}\n`)
209
+ } catch {
210
+ process.stderr.write(` FAIL ${e.name}\n`)
211
+ failed++
212
+ }
213
+ }
214
+ return failed === 0 ? 0 : 1
215
+ }
216
+
217
+ if (import.meta.main) process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,19 @@
1
+ # check-scenario-overlap
2
+
3
+ Internal SDD skill — the concrete engine for **cross-node scenario-overlap detection**. Audits across
4
+ the nodes of one project spec and emits the candidates where the **same behavior lives in more than
5
+ one node's `.feature`**, for the formation Warden: the intra-project **spec-level SSA** partner of the
6
+ collision ladder, and the cross-node sibling of `check-spec-structure` (intra-node node-shape).
7
+
8
+ ```bash
9
+ node scripts/check-scenario-overlap.mts --spec-dir <spec> # audit (TOON candidate set)
10
+ node scripts/check-scenario-overlap.mts --spec-dir <spec> --check # CI guard (fails on exact-duplicate)
11
+ ```
12
+
13
+ Two deterministic candidate kinds — **exact-duplicate** (blocking: two nodes share an identical
14
+ normalized fingerprint) and **title-overlap** (advisory: two nodes share a scenario title, differing
15
+ fingerprints) — plus a Warden `@rubric` arm that confirms real overlap and assigns a single owning
16
+ node. The fingerprint is step-bodies-only for a plain `Scenario`; a `Scenario Outline`'s fingerprint
17
+ also folds in its normalized `Examples` table (header + rows), since its steps are a template shared
18
+ by every canonical outline. Detection is cross-node only. Read-only, writes nothing.
19
+ See [`SKILL.md`](./SKILL.md) for the full contract. Not user-invocable.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: check-scenario-overlap
3
+ description: "Partial Skill: invoke by name only — project-spec/scenario-overlap's engine that detects the same behavior specified in two nodes' suites across one project spec — feeds the formation Warden, not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Check Scenario Overlap
10
+
11
+ The concrete engine for **cross-node scenario-overlap detection** — the intra-project **spec-level
12
+ SSA** partner of the code-side collision ladder. It audits **across the nodes** of one project spec
13
+ and returns the candidates where the **same behavior lives in more than one node's `.feature`**, for
14
+ the formation **Warden**. It is the **cross-node** sibling of `check-spec-structure` (intra-node
15
+ node-shape), whose per-node scope leaves this cross-node axis uncovered. Self-contained `.mts` (the
16
+ repo's node-≥23.6 / no-deps convention).
17
+
18
+ ## Why this rung exists
19
+
20
+ The scheduler treats two missions touching different `.feature` files as **file-disjoint** — but if
21
+ the **same behavior** is specified in **two** files, a change to that behavior must touch both, so
22
+ what looked disjoint is a **hard collision the scenario rung cannot see** (it diffs changed scenarios
23
+ per file, never across files). One behavior = **one scenario in one owning node** keeps the scenario
24
+ rung honest. Cross-*project* dedup (`dedupe-specs`) was retired when one project became one spec;
25
+ cross-*node* overlap **inside** a project had no detector until this one.
26
+
27
+ ## The two deterministic candidate kinds (and one judgment arm)
28
+
29
+ - **exact-duplicate** (blocking) — two **distinct** nodes whose suites each carry a scenario with an
30
+ **identical normalized fingerprint** (the ordered Given/When/Then step bodies, whitespace- and
31
+ case-normalized; for a `Scenario Outline`, its normalized `Examples` table too). A near-certain
32
+ one-behavior-two-nodes violation; `--check` fails on it.
33
+ - **title-overlap** (advisory) — two distinct nodes sharing a **normalized scenario title** but with
34
+ **differing** fingerprints. A weaker hint (same words may name different behavior); **never** fails
35
+ `--check`.
36
+ - **real-overlap + owning-node** is a **Warden judgment** (the spec's `@rubric` scenario) — the
37
+ engine ships no verdict. The Warden confirms the candidate is the same behavior (not a coincidental
38
+ text match) and **assigns a single owning node** for the dedup.
39
+
40
+ The fingerprint is computed from **step bodies only** for a plain `Scenario` — its title, tags,
41
+ comments, and the `.feature` prose never reach it, so the signal is behavior-shaped, not cosmetic. A
42
+ `Scenario Outline`'s steps are a **template**, not its content: every canonical `@trigger` outline
43
+ shares byte-identical steps by construction, so its fingerprint also folds in its **`Examples` table**
44
+ (header + rows, normalized cell-by-cell) — two outlines are an exact-duplicate only when their steps
45
+ **and** their rows match. Title and tags stay excluded from both shapes. Detection is **cross-node
46
+ only**: a scenario duplicated **within one node** raises no candidate, and a scenario appearing
47
+ **once** corpus-wide raises nothing.
48
+
49
+ ## Run the scan
50
+
51
+ ```bash
52
+ node "<skill>/scripts/check-scenario-overlap.mts" [--spec-dir <spec>] [--check] [--format toon|json]
53
+ ```
54
+
55
+ - Default `--spec-dir` is the current directory; default `--format` is **TOON** (the token-efficient
56
+ form the Warden scans).
57
+ - **Audit mode** (default) emits the candidate set — a `blocking[]` group then an `advisory[]` group,
58
+ each candidate naming **both** nodes and the overlapping scenario. `--format json` emits the same
59
+ candidates as a flat JSON array.
60
+ - **`--check`** (CI guard) exits **non-zero** iff an **exact-duplicate** candidate exists and **writes
61
+ nothing**; title-overlap advisories still exit zero. Wire it after `check-spec-structure --check` in
62
+ `verify:specs` so the project-spec holds the one-behavior-one-node partition.
63
+
64
+ When `node` is absent, an agent performs the same derivation by hand: parse each node's `.feature`
65
+ into scenarios, normalize each scenario's ordered steps into a fingerprint, and flag any fingerprint
66
+ (or, more weakly, any title) shared across two distinct node folders.
67
+
68
+ ## Boundaries
69
+
70
+ Steps + titles only — it never reads a scenario's prose into the fingerprint, owns no lifecycle
71
+ state, and writes nothing. It **never acts** on a candidate (a dedup, a relocation) — that is the
72
+ Warden's (`sdd:formation-loop`) under its own self-clear-vs-escalate verdict. It does **not** audit
73
+ node-shape (`check-spec-structure`), flag within-node duplicate scenarios, render the by-concept view
74
+ (`concept-index`), or advise a new node's home (`place-node`).