cyber-sdd 0.0.0 → 0.2.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.
- package/.plugin/pins.json +3 -0
- package/LICENSE +21 -0
- package/agents/sdd-automaton.md +13 -2
- package/agents/sdd-scanner.md +85 -0
- package/agents/sdd-spec-judge.md +32 -2
- package/agents/sdd-warden.md +9 -0
- package/package.json +30 -23
- package/skills/align-spec/scripts/align-spec.mts +3 -2
- package/skills/architect-spec-governance/README.md +1 -0
- package/skills/architect-spec-governance/SKILL.md +12 -1
- package/skills/blast-estimate/README.md +3 -5
- package/skills/blast-estimate/SKILL.md +2 -2
- package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
- package/skills/builder-impl-governance/SKILL.md +9 -1
- package/skills/builder-spec-governance/README.md +1 -0
- package/skills/builder-spec-governance/SKILL.md +31 -3
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
- package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
- package/skills/check-retired-terms/README.md +18 -0
- package/skills/check-retired-terms/SKILL.md +81 -0
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
- package/skills/collision-ladder/README.md +3 -5
- package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
- package/skills/combat-log-governance/SKILL.md +43 -4
- package/skills/concept-index/scripts/concept-index.mts +3 -2
- package/skills/discover-plans/scripts/discover-plans.mts +5 -2
- package/skills/discover-specs/scripts/discover-specs.mts +5 -2
- package/skills/doctrine-loop/README.md +6 -0
- package/skills/doctrine-loop/SKILL.md +136 -2
- package/skills/formation-loop/SKILL.md +21 -1
- package/skills/gate-validation-governance/SKILL.md +2 -2
- package/skills/impl-producer-governance/SKILL.md +10 -1
- package/skills/init/scripts/wire-statusline.mts +5 -2
- package/skills/lifecycle-governance/SKILL.md +1 -1
- package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
- package/skills/mission-graph/README.md +3 -5
- package/skills/mission-graph/SKILL.md +72 -5
- package/skills/mission-graph/scripts/mission-graph.mts +505 -16
- package/skills/oracle-spec-governance/README.md +7 -2
- package/skills/oracle-spec-governance/SKILL.md +21 -4
- package/skills/place-node/scripts/place-node.mts +3 -2
- package/skills/plan-retirement/README.md +5 -2
- package/skills/plan-retirement/SKILL.md +5 -1
- package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
- package/skills/plugin-contract-governance/SKILL.md +7 -1
- package/skills/remediation-governance/SKILL.md +36 -1
- package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
- package/skills/resolve-tracking/SKILL.md +2 -2
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
- package/skills/sdd/SKILL.md +1 -1
- package/skills/spec-format-governance/README.md +1 -1
- package/skills/spec-format-governance/SKILL.md +76 -8
- package/skills/spec-gate/SKILL.md +18 -2
- package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
- package/skills/spec-gate/scripts/check-suite.mts +53 -17
- package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
- package/skills/spec-producer-governance/README.md +1 -1
- package/skills/spec-producer-governance/SKILL.md +7 -3
- package/skills/ssa-lowering/README.md +3 -5
- package/skills/start-mission/README.md +1 -1
- package/skills/start-mission/SKILL.md +9 -5
- package/skills/suite-format-governance/SKILL.md +43 -4
- package/skills/touch-set-correction/README.md +3 -5
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
- package/skills/verify-scenarios/SKILL.md +10 -3
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// reaches the output. No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions
|
|
10
10
|
// are exported for node:test; running the file directly drives the CLI.
|
|
11
11
|
|
|
12
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
13
13
|
import { join } from 'node:path'
|
|
14
|
+
import { pathToFileURL } from 'node:url'
|
|
14
15
|
|
|
15
16
|
export const BEGIN_MARKER = '<!-- BEGIN generated: by-concept (project-spec/concept-index) -->'
|
|
16
17
|
export const END_MARKER = '<!-- END generated: by-concept -->'
|
|
@@ -240,6 +241,6 @@ export function main(argv: string[]): number {
|
|
|
240
241
|
return 0
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
if (import.meta.url ===
|
|
244
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
244
245
|
process.exit(main(process.argv.slice(2)))
|
|
245
246
|
}
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
// No dependencies (the repo's node-≥23.6 / no-deps convention). --format json for a flat
|
|
19
19
|
// array; default output is TOON (the token-efficient tabular form the gateway scans).
|
|
20
20
|
|
|
21
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
22
22
|
import { join } from 'node:path'
|
|
23
|
+
import { pathToFileURL } from 'node:url'
|
|
23
24
|
|
|
24
25
|
export const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed'])
|
|
25
26
|
|
|
@@ -209,4 +210,6 @@ export function main(argv: string[]): number {
|
|
|
209
210
|
return 0
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
if (import.meta.
|
|
213
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
214
|
+
process.exit(main(process.argv.slice(2)))
|
|
215
|
+
}
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
// default output is TOON (the token-efficient tabular form the gateway scans). --resolve <name>
|
|
27
27
|
// filters to the exact name matches (0 rows = none, 1 = resolved, >1 = ambiguous).
|
|
28
28
|
|
|
29
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
29
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
30
30
|
import { join } from 'node:path'
|
|
31
|
+
import { pathToFileURL } from 'node:url'
|
|
31
32
|
|
|
32
33
|
export const LIFECYCLE_STATUSES = new Set(['draft', 'approved', 'implemented', 'deprecated'])
|
|
33
34
|
|
|
@@ -393,4 +394,6 @@ export function main(argv: string[]): number {
|
|
|
393
394
|
return 0
|
|
394
395
|
}
|
|
395
396
|
|
|
396
|
-
if (import.meta.
|
|
397
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
398
|
+
process.exit(main(process.argv.slice(2)))
|
|
399
|
+
}
|
|
@@ -12,4 +12,10 @@ the one project `ledger/` directory. The entry **shape** and the matchable `caus
|
|
|
12
12
|
`sdd:combat-log-governance` (deferred, never restated). The Council holds keep-or-cut; the `sdd`
|
|
13
13
|
gateway surfaces the count of pending unratified strategy when the Council re-enters.
|
|
14
14
|
|
|
15
|
+
Separately, during its pass, the Scanner also cross-checks each plan brief's `todos-all-done`
|
|
16
|
+
against its `source-closed` to **derive the retirement clearance set** — it never autofixes a
|
|
17
|
+
plan's `status`; agreement feeds `sdd:plan-retirement`'s existing `--retire` input, disagreement
|
|
18
|
+
surfaces a flagged finding in the Scanner's pass summary. This is distinct from strategy-drafting
|
|
19
|
+
above: a flagged finding is never a ledger write.
|
|
20
|
+
|
|
15
21
|
Plan retirement (doctrine's last retro step) is the sibling `sdd:plan-retirement` skill.
|
|
@@ -46,6 +46,40 @@ Each is an entry-point: a lifecycle-grained trigger, its post-hoc input, and the
|
|
|
46
46
|
|
|
47
47
|
The Scanner **observes** the terminal transitions; it never writes a mission's `status`.
|
|
48
48
|
|
|
49
|
+
## Idempotent Ship/Kill: detect an already-distilled mission by parsing the ledger, never grepping
|
|
50
|
+
|
|
51
|
+
The **Ship** (`→ implemented`) and **Kill** (`→ deprecated`) triggers can meet the **same** terminal
|
|
52
|
+
transition more than once — a later pass, a resumed segment, a fresh re-invocation. So they are
|
|
53
|
+
**idempotent**: **before drafting** Ship/Kill strategy for a mission, first detect whether that
|
|
54
|
+
mission was **already distilled** — a prior `strategy` entry whose **`distills` field equals the
|
|
55
|
+
mission's `<cr-ref>`** — and if so, **draft nothing** (no duplicate strategy for a mission already
|
|
56
|
+
recorded as distilled).
|
|
57
|
+
|
|
58
|
+
**Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.**
|
|
59
|
+
The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather
|
|
60
|
+
than hand-rolling one: run the exported **`distilledCrRefs`** engine from
|
|
61
|
+
`../plan-retirement/scripts/retire-plans.mts` over the project `ledger/` directory and membership-test
|
|
62
|
+
the mission's `<cr-ref>` against the set it returns, e.g.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
node -e 'import("../plan-retirement/scripts/retire-plans.mts").then(m => console.log([...m.distilledCrRefs(process.argv[1])].join("\n")))' <ledger-dir>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`distilledCrRefs` parses **each line** with `JSON.parse` (keying only on `kind === "strategy"` with a
|
|
69
|
+
non-empty `distills`), so it is correct against **any** valid-JSON formatting the Scanner or another
|
|
70
|
+
writer emits — a **space after the colon** in a pretty-printed entry (`"distills": "<cr-ref>"` vs
|
|
71
|
+
`"distills":"<cr-ref>"`), a line-wrapped or reordered object — where a naive no-space substring grep
|
|
72
|
+
(`"distills":"<cr-ref>"`) **silently under-counts**, reports an already-distilled mission as
|
|
73
|
+
undistilled, and **re-drafts** it. This is the same defect class as *grep is blind to wrapped terms*;
|
|
74
|
+
a distilled-detection that under-counts causes false "undistilled" conclusions and wasted or duplicate
|
|
75
|
+
drafting.
|
|
76
|
+
|
|
77
|
+
Reusing the engine also inherits its exact semantics: it keys on the **structured `distills` field
|
|
78
|
+
only** (an `<cr-ref>` appearing merely inside another entry's `evidence` cross-references does **not**
|
|
79
|
+
count as distilled), **tolerates malformed and blank lines** (they are skipped, not fatal), and counts
|
|
80
|
+
an **unratified** distilling entry — the Scanner's default — the same as a ratified one (the question
|
|
81
|
+
is what was distilled, not sign-off). This is a **read** over the ledger; it changes nothing you write.
|
|
82
|
+
|
|
49
83
|
## Inputs: combat log (contract) vs transcripts (enrichment)
|
|
50
84
|
|
|
51
85
|
The Scanner reads **persisted artifacts post-hoc** — never live subagent context.
|
|
@@ -64,6 +98,81 @@ post-merge loop keeps the dimension; the **numeric** breakdown lives only in tra
|
|
|
64
98
|
never under it without a request). No raw token-cost number is written to the committed log — only
|
|
65
99
|
the categorical class (the safe-to-publish floor, `sdd:combat-log-governance`).
|
|
66
100
|
|
|
101
|
+
## Validate before drafting — a plan or log is a hypothesis, not present truth
|
|
102
|
+
|
|
103
|
+
A persisted plan, combat log, or ledger line is **history**, never authoritative present truth. Every
|
|
104
|
+
candidate improvement it surfaces — a gap to build, a defect to fix — is a **hypothesis about the
|
|
105
|
+
current codebase** (the repo's own principle: *a source-read is a hypothesis, refuted by a repro
|
|
106
|
+
against current code*). **Before you draft**, run a validation gate on each candidate: read the
|
|
107
|
+
CURRENT code and decide whether the flagged gap still exists, or has since been **built / fixed /
|
|
108
|
+
superseded** (by a later mission, a landed PR, a governance change).
|
|
109
|
+
|
|
110
|
+
- **Resolved → cut.** A candidate current code already resolves is **cut**: draft **no** build-or-fix
|
|
111
|
+
strategy and emit **no** issue. Record the cut **explicitly, never silently** — append a `strategy`
|
|
112
|
+
entry marked **`disposition: resolved`** to your own shard, carrying the **resolving current-code
|
|
113
|
+
evidence** (the repro that refuted the hypothesis) in `evidence`. A `disposition: resolved` line is
|
|
114
|
+
a **tombstone**: not an actionable recommendation, so it emits no issue and is **not counted toward
|
|
115
|
+
pending strategy** at the gateway. It exists so the cut is auditable and a later run does not
|
|
116
|
+
silently re-surface the same closed candidate.
|
|
117
|
+
- **Still open → draft.** A candidate current code does **not** resolve is a real improvement: draft
|
|
118
|
+
`strategy` for it marked **`disposition: open`** (the default; a line without the field grandfathers
|
|
119
|
+
as open), which counts toward pending strategy, and emit its issue (below).
|
|
120
|
+
|
|
121
|
+
The gate applies to any candidate that asserts an **unmet gap or defect**, whether surfaced by a
|
|
122
|
+
**plan or a combat log** (symmetric — each source has both a resolved→cut and a still-open→draft
|
|
123
|
+
path). A pure distilled **retro lesson** (a success pattern, not a gap claim) carries no gap to
|
|
124
|
+
validate and is drafted without a current-code check. Set the disposition **once at write** — never
|
|
125
|
+
flip it (append-only); the Council's later keep-or-cut on a `disposition: open` line is a separate act
|
|
126
|
+
(the keep-or-cut plan), not a field edit. The `disposition` field's shape is owned by
|
|
127
|
+
`sdd:combat-log-governance`.
|
|
128
|
+
|
|
129
|
+
This gate is what stops the loop reinforcing a **stale cache** — drafting "build X" for an X a later
|
|
130
|
+
mission already built.
|
|
131
|
+
|
|
132
|
+
## Cold-instrument evidence — non-author measurement + ablation before a rule-level recommendation
|
|
133
|
+
|
|
134
|
+
A self-authored measurement is untrustworthy exactly when it grounds a **rule-level decision**: the
|
|
135
|
+
author's own blind spot is baked into the instrument meant to catch it (op6/#224 — a masked generator
|
|
136
|
+
"proved" its own conclusion twice, each ratified then walked back; op6-m5/#254 — a proposed revival
|
|
137
|
+
measured Δ=0 dead weight, caught before landing). Two rules extend the validate-before-drafting gate to
|
|
138
|
+
your rule-level recommendations. This is **the canonical statement** of the non-author-evidence
|
|
139
|
+
standard — other faces (`sdd:spec-producer-governance`'s dimension/cut grounding) reference it.
|
|
140
|
+
|
|
141
|
+
- **Non-author evidence.** Draft a recommendation to **adopt a rule, drop a rule, or set a threshold**
|
|
142
|
+
only when its grounding measurement was **produced by a party other than the proposer, *or*
|
|
143
|
+
independently reviewed by such a party, *or* ablated against a freshly and adversarially constructed
|
|
144
|
+
case**. A measurement on the proposer's **own generator or harness alone** is **withheld**: draft no
|
|
145
|
+
recommendation on it, and record that it needs non-author or fresh-adversarial evidence. A measurement
|
|
146
|
+
grounding **no** rule-level decision is unconstrained by this rule.
|
|
147
|
+
- **Ablation before landing a revival.** Draft a candidate **reviving a dead measurement dimension** to
|
|
148
|
+
land only when an ablation against a control shows the dimension is **loseable**; **state the revived
|
|
149
|
+
rule abstractly with no worked example** (lifting a probe scenario's apparatus into the rule is
|
|
150
|
+
**absorption** — the probe then grades nothing). A revival whose ablation shows **no difference** from
|
|
151
|
+
the control is **dead weight**: **cut** it (a `disposition: resolved` tombstone, as above) and draft
|
|
152
|
+
no land recommendation.
|
|
153
|
+
|
|
154
|
+
## Improvement output — validated-open findings become tracked issues
|
|
155
|
+
|
|
156
|
+
The loop's **actionable output** for a validated-open improvement is a **new tracked issue**
|
|
157
|
+
(`gh issue create`) — one titled, bodied issue per real improvement, **cross-linking the evidence**
|
|
158
|
+
that drove it. This grounds the improvement plan in current code, not the stale narrative of a retired
|
|
159
|
+
plan. The ledger `strategy` line stays the **provenance**; the issue is what a later mission is started
|
|
160
|
+
from.
|
|
161
|
+
|
|
162
|
+
- **Dedupe first.** Before filing, dedupe against the forge's existing issues — **open and closed**
|
|
163
|
+
(at least two keyword combinations: the full title, then the core noun/verb) — and on a mixed set
|
|
164
|
+
file only the unmatched.
|
|
165
|
+
- **Emit is not dispatch.** Emitting an issue leaves the `strategy` **unratified** and spawns **no**
|
|
166
|
+
mission — it opens no CR and admits nothing to the mission graph (that is the graph's single writer's
|
|
167
|
+
act). Keep-or-cut stays the Council's; the issue re-enters SDD only when a **later** mission is
|
|
168
|
+
started from it.
|
|
169
|
+
- **Outward-publish floor.** Compose the issue body to the same outward-publish floor the handoff
|
|
170
|
+
follow-up issues meet (owned by the handoff unit — do not restate it): **self-contained** (a reader
|
|
171
|
+
who cannot see the mission's internal artifacts can act on it), **no production-internal artifact
|
|
172
|
+
reference** (no ledger shard filename, no combat-log or plan-brief path), plus everything the
|
|
173
|
+
committed-record floor bans (absolute paths, `$HOME`/`$USER`, usernames, secrets, raw numbers).
|
|
174
|
+
Carry an **agent-filed marker** and name the evidence it was distilled from.
|
|
175
|
+
|
|
67
176
|
## Where strategy lands
|
|
68
177
|
|
|
69
178
|
Every `strategy` entry lands in the **one project ledger** — the `ledger/` directory sibling of the
|
|
@@ -72,7 +181,9 @@ random hex once per session), so two concurrent Scanner runs write distinct shar
|
|
|
72
181
|
There is no per-spec log to route to under the project-spec model. The Scanner's `handle` is
|
|
73
182
|
`sdd-scanner`. Every entry is **unratified** (`ratified: false`) and carries its **driving evidence**
|
|
74
183
|
(the distilled `cause` recurrence that drove it), per the shape in `sdd:combat-log-governance`. The
|
|
75
|
-
shard is append-only — the next `seq` within it, never an edit; ledger lines carry **no `ts`**.
|
|
184
|
+
shard is append-only — the next `seq` within it, never an edit; ledger lines carry **no `ts`**. Every
|
|
185
|
+
entry also carries its validation **`disposition`** (`open` for a drafted still-open improvement,
|
|
186
|
+
`resolved` for a validation tombstone; see *Validate before drafting* above) — set once at write.
|
|
76
187
|
|
|
77
188
|
**Record the distilled subject.** When the entry is drafted from a **Ship** (`→ implemented`) or
|
|
78
189
|
**Kill** (`→ deprecated`), set `distills: <cr-ref>` to the **one mission it was distilled from** —
|
|
@@ -90,8 +201,31 @@ strategy re-enters as a **new CR** that re-tunes the doctrine and grows the corp
|
|
|
90
201
|
**PRUNE** removes the stale convention). On **cut**, it stays unratified and absent from the
|
|
91
202
|
corpus.
|
|
92
203
|
|
|
204
|
+
## Stale plan frontmatter — deriving the retirement clearance set, never autofixing status
|
|
205
|
+
|
|
206
|
+
A plan brief's frontmatter `status` can lag reality (its own todos finish, or its source issue
|
|
207
|
+
closes, without the value being flipped) but there is **no legal terminal value** to autofix it
|
|
208
|
+
into — the plan-level `status` is a two-value dispatch flag (`active | approved`), and the
|
|
209
|
+
contract's own answer to "this mission is over" is **retirement**, not a status write
|
|
210
|
+
(`sdd:combat-log-governance`'s home, `design/provenance-model.md`). So during its pass, for each
|
|
211
|
+
brief under `.agents/plans/`, the Scanner cross-checks `todos-all-done` (every `todos[].status`
|
|
212
|
+
completed) against `source-closed` (the declared `source` queried natively, the same way
|
|
213
|
+
`plan-retirement`'s own clearance check queries it) — never writing `status` either way:
|
|
214
|
+
|
|
215
|
+
- **Both agree terminal** → the cr-ref is included in the **retirement clearance set** the Scanner
|
|
216
|
+
passes as `plan-retirement`'s existing `--retire` input. No new deletion mechanism —
|
|
217
|
+
`plan-retirement` still runs its own gated sweep.
|
|
218
|
+
- **Both agree non-terminal** → no clearance, no finding.
|
|
219
|
+
- **They disagree** → excluded from the clearance set; the Scanner surfaces a flagged finding
|
|
220
|
+
naming the disagreement in its pass summary (its only channel — it returns only its final
|
|
221
|
+
message), for a human to resolve. A flagged finding is ephemeral and never a ledger write — it
|
|
222
|
+
is neither a `kind: strategy` entry nor a `kind: report` line, and it is distinct from the
|
|
223
|
+
validated-open-improvement finding above that becomes a tracked issue.
|
|
224
|
+
|
|
93
225
|
## Plan retirement
|
|
94
226
|
|
|
95
227
|
Doctrine's **last retro step** — the gated, idempotent **tracked deletion** of a retired plan — is
|
|
96
228
|
a separate unit (`sdd:plan-retirement`). The distill (writing `strategy` here) fires early, at
|
|
97
|
-
`→ implemented`; the delete is a later step gated on source = `done`/merged **and** distilled.
|
|
229
|
+
`→ implemented`; the delete is a later step gated on source = `done`/merged **and** distilled. The
|
|
230
|
+
source-cleared set `plan-retirement` consumes via `--retire` is, per above, the Scanner's derived
|
|
231
|
+
retirement clearance set — never a raw, uncross-checked source-only judgment.
|
|
@@ -67,7 +67,7 @@ structure and discovery (`corpus/` + `project-spec/`), not on any given station
|
|
|
67
67
|
| Act | Trigger | Station (`corpus/` + `project-spec/`) | Output |
|
|
68
68
|
|---|---|---|---|
|
|
69
69
|
| **Audit node-shape** | a formation pass fires post-mission | `check-spec-structure` | a finding set: untagged-node (blocking) + oversized-node (advisory), each naming the node |
|
|
70
|
-
| **Split an oversized node** | the Warden's `@rubric` breadth-vs-depth judgment routes an oversized-node's shape profile to breadth-overflow | `check-spec-structure` | a sub-node split; depth-overflow instead down-levels via the scenario→test bridge (`verify-scenarios`) or is redesigned — the engine emits only the profile, never the route |
|
|
70
|
+
| **Split an oversized node** | the Warden's `@rubric` breadth-vs-depth judgment routes an oversized-node's shape profile to breadth-overflow | `check-spec-structure` | a sub-node split whose **organizing axis is first checked against a real capability/command boundary** (below); depth-overflow instead down-levels via the scenario→test bridge (`verify-scenarios`) or is redesigned — the engine emits only the profile, never the route |
|
|
71
71
|
| **Reconcile drift / contradiction** | prose↔suite drift, or two nodes contradict | `align-spec` | a reconcile finding (drift fixed by direction; contradiction → align the losing side) |
|
|
72
72
|
| **Dedupe cross-node scenario overlap** | the same behavior is specified in two nodes' suites — a hard collision the scenario rung cannot see (spec-level SSA) | `check-scenario-overlap` | a dedup finding naming both nodes; the Warden's `@rubric` arm confirms real overlap and **assigns a single owning node** (one behavior = one scenario in one node) |
|
|
73
73
|
|
|
@@ -76,6 +76,26 @@ raises **no** untagged finding; nodes (or governances) that **agree** raise **no
|
|
|
76
76
|
two nodes that specify **no** shared behavior raise **no** dedup finding. The acts are evidence-gated,
|
|
77
77
|
not run unconditionally.
|
|
78
78
|
|
|
79
|
+
**The split-axis check — validate the organizing axis, not just the granularity.** An oversized node
|
|
80
|
+
is a **granularity** signal, but a split carves it along a proposed **organizing axis**, and the
|
|
81
|
+
wrong axis produces a split CR that gets **superseded rather than landed** — a full formation cycle
|
|
82
|
+
wasted. So before proposing a split (self-clearing it or escalating its CR), the Warden **checks the
|
|
83
|
+
proposed axis against a real capability/command boundary**: each side must map to a **distinct
|
|
84
|
+
capability, command, or lifecycle phase**, not merely to an **internal implementation grouping** that
|
|
85
|
+
shares one boundary.
|
|
86
|
+
|
|
87
|
+
- an axis where each side is a distinct capability/command boundary **passes** — the split is
|
|
88
|
+
proposed on that axis;
|
|
89
|
+
- an axis that only regroups internal implementation under one shared boundary **fails** — the Warden
|
|
90
|
+
does **not** carve sub-nodes on it, and raises the oversize as a **wrong-axis reorganization** (the
|
|
91
|
+
axis is wrong), not a granularity split to carve as-proposed.
|
|
92
|
+
|
|
93
|
+
An oversize can be a symptom of the **wrong axis**, not just wrong granularity: turn "this node is too
|
|
94
|
+
big" into "too big **along which axis** — and is that axis real?" (Precedent: a killed
|
|
95
|
+
`identity/`→`presence/` split proposed a plausible-but-unreal axis and was superseded by a realignment
|
|
96
|
+
that split along the package's actual command boundary; the producer/consumer boundary had even been
|
|
97
|
+
validated as sound, yet the *split axis* was never checked against a real command boundary.)
|
|
98
|
+
|
|
79
99
|
Alongside its findings a pass surfaces an **advisory layout-quality signal** — the scheduler's
|
|
80
100
|
**false-conflict rate** doubles as a **partition-quality metric**: a layout that keeps node↔folder
|
|
81
101
|
clean holds the rate low, and one that scatters a single concern across folders drives it up. Read
|
|
@@ -25,7 +25,7 @@ rules by reading frontmatter. The `(status, markers, .feature, approval)` tuple
|
|
|
25
25
|
- an `approval.<gate>` is `verdict: pause` carrying a `by` — a pause is always the agent's act and omits `by`.
|
|
26
26
|
- an `approval.<gate>` is `verdict: approve` with no `by` — an approve must record its approver.
|
|
27
27
|
- an `approval.<gate>` is `by: agent` with no `why` block — a self-assertion must record its derivation.
|
|
28
|
-
- an `approval.<gate>` has
|
|
28
|
+
- an `approval.<gate>` has an off-enum `cause` (not `dimension` / `clearance` / `ceiling`) **without** a `cause-candidate: true` flag — the stop cause is a closed enum grown only by ratification, so an unflagged off-enum value fails closed; a `cause-candidate: true`-flagged off-enum value is **legal** (the off-enum candidate discipline, `sdd:combat-log-governance`).
|
|
29
29
|
- an `approval.<gate>` is `verdict: pause` on a gate the spec has already passed (spec once `approved`/`implemented`, impl once `implemented`).
|
|
30
30
|
- `status: approved` or `implemented` with no `approval.spec` `verdict: approve` + `by` — the spec gate has no recorded ratification.
|
|
31
31
|
- `status: implemented` with no `approval.impl` `verdict: approve` + `by` — the impl gate has no recorded ratification.
|
|
@@ -83,5 +83,5 @@ A required production role **always** resolves to a real producer — a plugin a
|
|
|
83
83
|
for that role. When a gate runs and a required role has **no resolvable producer** (not a plugin
|
|
84
84
|
agent and not even an SDD default), the gate **fails closed** with a blocker; it advances nothing.
|
|
85
85
|
This is a **structural** error, the same fail-closed class as a malformed `produced-by` entry or an
|
|
86
|
-
off-enum `cause` (
|
|
86
|
+
**unflagged** off-enum `cause` (a `cause-candidate: true`-flagged one is legal; `sdd:combat-log-governance`). Distinct from availability: a recorded
|
|
87
87
|
producer whose plugin is merely uninstalled is **flagged**, not blocked.
|
|
@@ -47,6 +47,14 @@ MODE: explore | implement
|
|
|
47
47
|
the inner rules — the pyramid's base, separate from the per-scenario duty. A non-deterministic
|
|
48
48
|
subject has no such layer — verify at the acceptance level only.
|
|
49
49
|
|
|
50
|
+
**Instrument subject → mutation-sweep-first (the cold-instrument doctrine).** When the **subject is
|
|
51
|
+
itself a measurement or verification instrument** — a fixture, mutation set, ablation generator,
|
|
52
|
+
falsifier, judge, or check — a **mutation sweep is the default verification method** and reading the
|
|
53
|
+
instrument is **supplementary**, not the primary check. This **overrides**, for an instrument subject
|
|
54
|
+
only, the verify-as-high default above; a non-instrument subject keeps that default unchanged.
|
|
55
|
+
Reading an instrument finds a defect or two where a sweep finds many, and a "cannot-fail" defect that
|
|
56
|
+
survives every read dies to the first mutation the instrument fails to catch.
|
|
57
|
+
|
|
50
58
|
5. **Never modify `spec.md` or the suite** — four-eyes. A behavior-changing gap is a
|
|
51
59
|
`CONTENT_GAP` / `BLOCKER`, never an in-place edit. Never change or remove a `@pinned` scenario —
|
|
52
60
|
propose it and surface for user authorization (`sdd:ownership-governance`).
|
|
@@ -80,6 +88,7 @@ OBSERVATIONS: [ { owner: architect | strategist, note, evidence } ]
|
|
|
80
88
|
3. **Author one check per frozen scenario** anchored to it; prefer running the scenario directly so
|
|
81
89
|
the oracle stays spec-owned; an unverifiable scenario is a reported gap.
|
|
82
90
|
4. **Verify as high as it doesn't hurt** — record level + why; deterministic combinatorics go to unit
|
|
83
|
-
tests (the pyramid base).
|
|
91
|
+
tests (the pyramid base). **An instrument subject** (fixture / mutation set / ablation generator /
|
|
92
|
+
falsifier / judge / check) inverts the default: **mutation-sweep-first**, reading supplementary.
|
|
84
93
|
5. **Never modify `spec.md` / the suite; never touch a `@pinned` scenario without user
|
|
85
94
|
authorization.**
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
// fresh wire ever adopts it.
|
|
26
26
|
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
|
|
27
27
|
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
29
29
|
import { homedir } from 'node:os'
|
|
30
30
|
import { dirname, join } from 'node:path'
|
|
31
|
+
import { pathToFileURL } from 'node:url'
|
|
31
32
|
|
|
32
33
|
export const STATUS_FILE = '.agents/sdd/statusline'
|
|
33
34
|
export const SETTINGS_FILE = '.claude/settings.json'
|
|
@@ -273,4 +274,6 @@ export function main(argv: string[]): number {
|
|
|
273
274
|
return 0
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
if (import.meta.
|
|
277
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
278
|
+
process.exit(main(process.argv.slice(2)))
|
|
279
|
+
}
|
|
@@ -24,7 +24,7 @@ approval: # per-gate verdict
|
|
|
24
24
|
spec: # verdict: approve | pause | reject
|
|
25
25
|
verdict: approve
|
|
26
26
|
by: agent # agent (self-asserted, provisional) | <human name> (ratified); omitted on pause
|
|
27
|
-
cause: dimension # dimension | ceiling — what drove the verdict
|
|
27
|
+
cause: dimension # dimension | clearance | ceiling — what drove the verdict (off-enum → flag cause-candidate: true)
|
|
28
28
|
why: # verdict derivation (agent self-assertion or pause)
|
|
29
29
|
floor: <none | clearance | conflict | compatibility | consent>
|
|
30
30
|
blast: <low|high — reason>
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps
|
|
18
18
|
// (the repo's node-≥23.6 convention).
|
|
19
19
|
|
|
20
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { dirname, join } from 'node:path'
|
|
22
|
+
import { pathToFileURL } from 'node:url'
|
|
22
23
|
|
|
23
24
|
const IGNORE_FILE = '.agents/sdd/.sddignore'
|
|
24
25
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
|
|
@@ -291,4 +292,6 @@ export function main(argv: string[]): number {
|
|
|
291
292
|
return 0
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
if (import.meta.
|
|
295
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
296
|
+
process.exit(main(process.argv.slice(2)))
|
|
297
|
+
}
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps (the
|
|
23
23
|
// repo's node-≥23.6 convention).
|
|
24
24
|
|
|
25
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
26
26
|
import { dirname, join } from 'node:path'
|
|
27
|
+
import { pathToFileURL } from 'node:url'
|
|
27
28
|
import { parseSourcesToml, type SourceConfig } from '../../verify-scenarios/scripts/verify-scenarios.mts'
|
|
28
29
|
|
|
29
30
|
export type { SourceConfig }
|
|
@@ -153,4 +154,6 @@ export function main(argv: string[]): number {
|
|
|
153
154
|
return 0
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
if (import.meta.
|
|
157
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
158
|
+
process.exit(main(process.argv.slice(2)))
|
|
159
|
+
}
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// are implicit and cannot be curated here.
|
|
18
18
|
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
|
|
19
19
|
|
|
20
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { dirname, join } from 'node:path'
|
|
22
|
+
import { pathToFileURL } from 'node:url'
|
|
22
23
|
|
|
23
24
|
const ANCHORS_CONFIG = '.agents/sdd/spec-anchors.toml'
|
|
24
25
|
const LIFECYCLE_STATUSES = new Set(['draft', 'approved', 'implemented', 'deprecated'])
|
|
@@ -325,4 +326,6 @@ export function main(argv: string[]): number {
|
|
|
325
326
|
return 0
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
if (import.meta.
|
|
329
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
330
|
+
process.exit(main(process.argv.slice(2)))
|
|
331
|
+
}
|
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
The concrete engine for the **mission-graph kernel** — a git-tracked, append-only work-graph store
|
|
4
4
|
(nodes, edges, status, tombstones, schema `v:1`) folded into a read-only `ready` frontier and a
|
|
5
5
|
`cycles` repair view, plus an Operation deliverability check. Built for Op1.M1 of the
|
|
6
|
-
**cyberfleet-batch** change request;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[`mission-graph.feature`](../../../../.agents/specs/sdd/mission-graph/mission-graph.feature) for
|
|
10
|
-
the frozen 36-scenario contract.
|
|
6
|
+
**cyberfleet-batch** change request; the `mission-graph` node of the SDD project spec (in the
|
|
7
|
+
cyberplace repository, not shipped in this package) carries the authoritative behavior description
|
|
8
|
+
and the frozen 36-scenario contract.
|
|
11
9
|
|
|
12
10
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
13
11
|
- **Script:** [`scripts/mission-graph.mts`](./scripts/mission-graph.mts)
|
|
@@ -25,8 +25,9 @@ plus a zero-dependency **fold** into two read-only views and one write-time guar
|
|
|
25
25
|
|
|
26
26
|
It carries a self-contained `.mts` script (the repo's node-≥23.6 / no-deps convention). Pure
|
|
27
27
|
derivations (`fold`, `ready`, `cycles`, `checkOperation`, `proposeEdge`) take and return plain data
|
|
28
|
-
only — no fs access — kept apart from a thin store-IO seam
|
|
29
|
-
|
|
28
|
+
only — no fs access — kept apart from a thin store-IO seam with **two backends**: an in-tree JSONL
|
|
29
|
+
file and the branch-independent orphan ref `refs/sdd/mission-graph`, resolved at run time. The swap
|
|
30
|
+
never touches a derivation.
|
|
30
31
|
|
|
31
32
|
## Run it
|
|
32
33
|
|
|
@@ -45,11 +46,77 @@ node "<skill>/scripts/mission-graph.mts" append edge --kind RAW|parent-child|dis
|
|
|
45
46
|
|
|
46
47
|
node "<skill>/scripts/mission-graph.mts" append tombstone --target node --id <id>
|
|
47
48
|
node "<skill>/scripts/mission-graph.mts" append tombstone --target edge --kind <kind> --from <id> --to <id>
|
|
49
|
+
|
|
50
|
+
# store home + reach (idempotent; --root defaults to .)
|
|
51
|
+
node "<skill>/scripts/mission-graph.mts" migrate [--root .]
|
|
52
|
+
node "<skill>/scripts/mission-graph.mts" sync [--root .] [--remote origin]
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
- Default `--root` is the current directory
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
- Default `--root` is the current directory. The store home is resolved at run time — the orphan ref
|
|
56
|
+
`refs/sdd/mission-graph` inside a git work-tree, else the in-tree file
|
|
57
|
+
`<root>/.agents/mission-graph/events.jsonl`; `MISSION_GRAPH_STORE=in-tree|orphan-ref` overrides.
|
|
58
|
+
Default `--format` is **TOON**; `--format json` emits the same records as JSON for non-LLM
|
|
59
|
+
consumers.
|
|
60
|
+
- **`migrate` retires the in-tree seed** after copying it into the ref — `git rm` the store file,
|
|
61
|
+
leaving the deletion **staged, not committed** (the engine's only working-tree write; it reports that
|
|
62
|
+
the commit is required for the migration to reach other clones). **Why it is not optional:** the seed
|
|
63
|
+
is a *tracked* file, so a seed left behind travels to every clone, where `resolveBackend` prefers it
|
|
64
|
+
over the (never-fetched) ref — the clone reads a **stale list that looks valid** and no `sync` can
|
|
65
|
+
dislodge it, because the seed is precisely what stops the clone consulting the ref. Two guards:
|
|
66
|
+
it **refuses** to retire a seed whose every line is not already in the ref (deleting otherwise would
|
|
67
|
+
be the data loss migration exists to avoid), and it **does** retire a seed an earlier migration left
|
|
68
|
+
behind, so a pre-fix project is repaired by re-running `migrate`.
|
|
69
|
+
- **Reach.** `refs/sdd/*` is outside `refs/heads/*`, so git's default refspec never fetches or
|
|
70
|
+
pushes it: the ref is shared by every **worktree of one clone** and travels no further on its own.
|
|
71
|
+
`sync` is the one step that widens that reach.
|
|
72
|
+
- **`sync` names the ref explicitly on its own command line**, so it needs **no git configuration and
|
|
73
|
+
writes none** — not a fetch refspec, not a push refspec. Verified: a fresh clone carrying only the
|
|
74
|
+
default `+refs/heads/*:refs/remotes/origin/*` fetches the store ref fine when the refspec is given
|
|
75
|
+
as an argument. The engine never touches `.git/config`.
|
|
76
|
+
- **No refspec is ever written in its forced (`+`-prefixed) form.** A leading `+` means *force*: with
|
|
77
|
+
`+refs/sdd/*:refs/sdd/*` in play, a diverged store ref is overwritten at **exit 0** with
|
|
78
|
+
`(forced update)`, silently destroying the local appends — the exact data loss the compare-and-swap
|
|
79
|
+
write path exists to prevent. Non-forced, the same transfer reports
|
|
80
|
+
`! [rejected] (non-fast-forward)` and **exits 1**, leaving the ref intact.
|
|
81
|
+
- `sync` is **fast-forward only**, in both directions. Its cases are a **total partition** of
|
|
82
|
+
`(local ref, remote ref)` — each side present or absent, and when both are present the pair is
|
|
83
|
+
exactly one of identical / one an ancestor of the other (either way) / neither:
|
|
84
|
+
|
|
85
|
+
Two checks run **before** the partition, in this order — (1) **backend**: under the in-tree backend,
|
|
86
|
+
no-op reporting the home, exit 0, no network touched (an in-tree project must not fail merely for
|
|
87
|
+
being offline); (2) **reachability**: a remote unreachable *or not configured* fails non-zero and is
|
|
88
|
+
**never** reported as "remote has none". Then:
|
|
89
|
+
|
|
90
|
+
| local | remote | action |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| absent | absent | report **both absent**, change nothing |
|
|
93
|
+
| present | absent | push |
|
|
94
|
+
| absent | present | collect |
|
|
95
|
+
| present | present, same commit | report agreement, change nothing |
|
|
96
|
+
| present | present, local is ancestor | fast-forward local |
|
|
97
|
+
| present | present, remote is ancestor | push |
|
|
98
|
+
| present | present, neither is ancestor | **refuse loudly**, report both tips, change nothing |
|
|
99
|
+
|
|
100
|
+
There is no `--force`; divergence means two write-deciders, which the store surfaces rather than
|
|
101
|
+
resolves. **The reachability row is load-bearing:** `git ls-remote` fails the same way for an
|
|
102
|
+
unreachable remote as for a missing ref, so a naive read conflates them and lands on "both absent" —
|
|
103
|
+
reporting a benign empty state for a remote it never saw. That is the silent-empty defect this whole
|
|
104
|
+
capability exists to remove; establish reachability first and fail non-zero when it cannot be
|
|
105
|
+
established.
|
|
106
|
+
- **Making an ordinary `git fetch` collect the ref too is out of scope here.** That means writing a
|
|
107
|
+
persistent fetch refspec into a clone's config — an opt-in, per-clone setup act with a real cost (a
|
|
108
|
+
diverged store ref then makes unrelated `git fetch` calls exit 1). It belongs to the onboarding
|
|
109
|
+
skill that already asks consent before writing operational config, not to this engine.
|
|
110
|
+
- **Recovering from a refused sync.** The refusal names both tips. Read each side's log with
|
|
111
|
+
`git cat-file -p <tip>:events.jsonl`, then — as the single write-decider — rebuild one list holding
|
|
112
|
+
both sides' events and append it forward from the tip you keep. The store is append-only, so no
|
|
113
|
+
event is dropped by that reconcile; `sync` does not automate it, because choosing the surviving
|
|
114
|
+
order is a decision, not a merge.
|
|
115
|
+
- **Never configure `remote.<remote>.push`.** Setting it *replaces* the default branch push refspec,
|
|
116
|
+
so a plain `git push` silently stops sending the current branch while reporting
|
|
117
|
+
"Everything up-to-date". `sync` passes its push refspec as an argument and writes no push config.
|
|
118
|
+
- **`sync` is orphan-ref-only.** Under the in-tree backend it is a **no-op** that reports the active
|
|
119
|
+
backend — an in-tree store rides its branch and needs no ref transport.
|
|
53
120
|
- `append edge` runs the **write-time cycle guard** first: a RAW edge that would close a loop is
|
|
54
121
|
**rejected** unless `--override` is passed (a genuinely-discovered mutual dependency). A
|
|
55
122
|
parent-child/discovered-from edge is never guarded — only RAW ("wait for") edges can cycle.
|