cyber-sdd 0.6.1 → 0.7.1
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/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/agents/sdd-scanner.md +1 -1
- package/agents/sdd-spec-judge.md +3 -3
- package/agents/sdd-warden.md +8 -0
- package/package.json +11 -13
- package/plugin.json +55 -0
- package/skills/align-spec/README.md +3 -3
- package/skills/align-spec/SKILL.md +2 -2
- package/skills/align-spec/scripts/align-spec.mjs +6755 -0
- package/skills/architect-impl-governance/SKILL.md +3 -1
- package/skills/architect-spec-governance/SKILL.md +5 -1
- package/skills/blast-estimate/README.md +4 -4
- package/skills/blast-estimate/SKILL.md +1 -1
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -0
- package/skills/check-field-mandates/SKILL.md +1 -1
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -0
- package/skills/check-partition-quality/SKILL.md +1 -1
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -0
- package/skills/check-plan-safety/README.md +3 -3
- package/skills/check-plan-safety/SKILL.md +3 -4
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -0
- package/skills/check-plugin-manifests/SKILL.md +1 -1
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -0
- package/skills/check-project-specs/README.md +3 -3
- package/skills/check-project-specs/SKILL.md +3 -3
- package/{dist/check-project-specs/scripts/check-project-specs.js → skills/check-project-specs/scripts/check-project-specs.mjs} +300 -18
- package/skills/check-retired-terms/README.md +2 -2
- package/skills/check-retired-terms/SKILL.md +4 -5
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -0
- package/skills/check-scenario-overlap/README.md +2 -2
- package/skills/check-scenario-overlap/SKILL.md +2 -3
- package/{dist/check-scenario-overlap/scripts/check-scenario-overlap.js → skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs} +3 -1
- package/skills/check-spec-references/README.md +1 -1
- package/skills/check-spec-references/SKILL.md +3 -3
- package/{dist/check-spec-references/scripts/check-spec-references.js → skills/check-spec-references/scripts/check-spec-references.mjs} +3 -1
- package/skills/check-spec-structure/README.md +2 -2
- package/skills/check-spec-structure/SKILL.md +2 -3
- package/{dist/check-spec-structure/scripts/check-spec-structure.js → skills/check-spec-structure/scripts/check-spec-structure.mjs} +3 -1
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/SKILL.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -0
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/README.md +2 -2
- package/skills/concept-index/SKILL.md +2 -2
- package/{dist/concept-index/scripts/concept-index.js → skills/concept-index/scripts/concept-index.mjs} +3 -1
- package/skills/discover-plans/README.md +5 -5
- package/skills/discover-plans/SKILL.md +2 -2
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -0
- package/skills/discover-specs/README.md +4 -4
- package/skills/discover-specs/SKILL.md +2 -2
- package/{dist/discover-specs/scripts/discover-specs.js → skills/discover-specs/scripts/discover-specs.mjs} +4 -1
- package/skills/doctrine-loop/SKILL.md +2 -2
- package/skills/formation-loop/SKILL.md +10 -0
- package/skills/gate-validation-governance/SKILL.md +2 -2
- package/skills/init/README.md +5 -5
- package/skills/init/SKILL.md +3 -3
- package/skills/init/scripts/wire-statusline.mjs +168 -0
- package/skills/manage-ignore/README.md +8 -8
- package/skills/manage-ignore/SKILL.md +2 -3
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -0
- package/skills/manage-scenario-bridge/README.md +6 -6
- package/skills/manage-scenario-bridge/SKILL.md +2 -2
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -0
- package/skills/manage-spec-anchors/README.md +7 -7
- package/skills/manage-spec-anchors/SKILL.md +2 -2
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -0
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +10 -10
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -0
- package/skills/pause-mission/SKILL.md +2 -2
- package/skills/place-node/README.md +1 -1
- package/skills/place-node/SKILL.md +3 -3
- package/skills/place-node/scripts/place-node.mjs +117 -0
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/SKILL.md +1 -1
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -0
- package/skills/resolve-governances/README.md +4 -4
- package/skills/resolve-governances/SKILL.md +2 -2
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -0
- package/skills/resolve-tracking/SKILL.md +2 -2
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -0
- package/skills/scaffold-project-spec/SKILL.md +3 -3
- package/skills/sdd/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +6 -6
- package/{dist/spec-gate/scripts/check-spec-state.js → skills/spec-gate/scripts/check-spec-state.mjs} +3 -1
- package/{dist/spec-gate/vendor/gherkin-cli.js → skills/spec-gate/scripts/check-suite.mjs} +319 -188
- package/skills/spec-gate/{vendor/gherkin-cli.mjs → scripts/classify-edit-class.mjs} +243 -209
- package/skills/spec-producer-governance/SKILL.md +4 -4
- package/skills/spec-structure-governance/README.md +4 -2
- package/skills/spec-structure-governance/SKILL.md +42 -7
- package/skills/start-mission/SKILL.md +2 -2
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +2 -2
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -0
- package/skills/verify-scenarios/README.md +9 -9
- package/skills/verify-scenarios/SKILL.md +2 -2
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -0
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -369
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -660
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -199
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -420
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -242,7 +242,7 @@ plan/log-surfaced candidate against **current code** (a persisted plan or log is
|
|
|
242
242
|
closed candidate.
|
|
243
243
|
|
|
244
244
|
```jsonl
|
|
245
|
-
{"seq": 3, "handle": "sdd-scanner", "kind": "strategy", "disposition": "resolved", "recommendation": "no action — coverage-gap mechanism already shipped", "evidence": ["current-code: checkReferencedArtifacts + checkUseCaseCoverage live in spec-gate/scripts/check-spec-state.
|
|
245
|
+
{"seq": 3, "handle": "sdd-scanner", "kind": "strategy", "disposition": "resolved", "recommendation": "no action — coverage-gap mechanism already shipped", "evidence": ["current-code: checkReferencedArtifacts + checkUseCaseCoverage live in spec-gate/scripts/check-spec-state.mjs"], "ratified": false}
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
`disposition` is set **once at write** and never flipped (the append-only invariant) — the Scanner's
|
|
@@ -5,8 +5,8 @@ every node's `concept:` frontmatter and renders the **by-concept view** into the
|
|
|
5
5
|
re-unifying a cross-cutting concern the capability folder tree scatters.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
node scripts/concept-index.
|
|
9
|
-
node scripts/concept-index.
|
|
8
|
+
node scripts/concept-index.mjs --spec-dir <spec> --write # refresh the block
|
|
9
|
+
node scripts/concept-index.mjs --spec-dir <spec> --check # no-drift guard
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Pure derivation, frontmatter only; the write touches only the delimited generated block. See
|
|
@@ -12,12 +12,12 @@ The concrete engine for the **concept-index** step. It scans
|
|
|
12
12
|
one project-spec for every node's `concept:` frontmatter and renders the **by-concept view** —
|
|
13
13
|
`concept → {its nodes across every folder}` — that re-unifies a cross-cutting concern the capability
|
|
14
14
|
folder tree scatters (the concept axis: one concern enacted across several capability folders). It
|
|
15
|
-
carries a self-contained `.
|
|
15
|
+
carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
16
16
|
|
|
17
17
|
## Run it
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
node "<skill>/scripts/concept-index.
|
|
20
|
+
node "<skill>/scripts/concept-index.mjs" --spec-dir <spec> [--write | --check]
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
- default (no mode) — print the rendered "By concept" section to stdout (dry run).
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/concept-index/concept-index.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/concept-index/scripts/concept-index.mjs","commands":{}};
|
|
2
4
|
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
5
|
import { join, relative } from "node:path";
|
|
4
6
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region
|
|
7
|
+
//#region src/concept-index/concept-index.mts
|
|
6
8
|
const BEGIN_MARKER = "<!-- BEGIN generated: by-concept (project-spec/concept-index) -->";
|
|
7
9
|
const END_MARKER = "<!-- END generated: by-concept -->";
|
|
8
10
|
const ANCHOR_HEADING = "## Invariants";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# discover-plans
|
|
2
2
|
|
|
3
3
|
The concrete engine for SDD **plan discovery**. A non-user-invocable skill carrying a self-contained
|
|
4
|
-
`.
|
|
4
|
+
`.mjs` script that scans `.agents/plans` for `*.plan.md` mission briefs, treats each present brief as
|
|
5
5
|
an unretired/resumable mission, tallies its todos, reads its top-level `status` dispatch flag
|
|
6
6
|
(`active` when unset) and its `## NEXT` resume lead, and emits a TOON list. The plan sibling of
|
|
7
7
|
[`discover-specs`](../discover-specs/README.md).
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/discover-plans.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/discover-plans.mjs`](./scripts/discover-plans.mjs)
|
|
11
|
+
- **Tests:** [`src/discover-plans/discover-plans.test.mts`](../../src/discover-plans/discover-plans.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
node scripts/discover-plans.
|
|
15
|
-
node scripts/discover-plans.
|
|
14
|
+
node scripts/discover-plans.mjs --root . --format toon
|
|
15
|
+
node scripts/discover-plans.mjs --root . --status approved # the dispatch queue: approved briefs only
|
|
16
16
|
```
|
|
@@ -12,7 +12,7 @@ The concrete engine for SDD **plan discovery**. It
|
|
|
12
12
|
locates the **resumable missions** in a repo — the mission **plan briefs** under `.agents/plans` —
|
|
13
13
|
and returns each one's CR ref, name, todo tally, and `## NEXT` resume lead, **without reading the
|
|
14
14
|
rest of the body**, so a consumer (the **gateway**, on entry) can offer resume cheaply. It carries a
|
|
15
|
-
self-contained `.
|
|
15
|
+
self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11). It is the **plan** sibling
|
|
16
16
|
of `discover-specs`: that engine finds **specs** by status shape; this one finds **missions** by
|
|
17
17
|
their briefs.
|
|
18
18
|
|
|
@@ -42,7 +42,7 @@ the flag; it never interprets or writes it.
|
|
|
42
42
|
## Run the scan
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
node "<skill>/scripts/discover-plans.
|
|
45
|
+
node "<skill>/scripts/discover-plans.mjs" [--root .] [--format toon|json] [--status <value>]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
- Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/discover-plans/discover-plans.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/discover-plans/scripts/discover-plans.mjs","commands":{}};
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
//#region src/discover-plans/discover-plans.mts
|
|
8
|
+
const TODO_STATUSES = /* @__PURE__ */ new Set([
|
|
9
|
+
"pending",
|
|
10
|
+
"in_progress",
|
|
11
|
+
"completed"
|
|
12
|
+
]);
|
|
13
|
+
/** The default mission dispatch status — the meaning of an absent top-level `status`. */
|
|
14
|
+
const DEFAULT_PLAN_STATUS = "active";
|
|
15
|
+
function parsePlanFrontmatter(text) {
|
|
16
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
17
|
+
if (!m) return null;
|
|
18
|
+
const fm = {
|
|
19
|
+
name: "",
|
|
20
|
+
status: DEFAULT_PLAN_STATUS,
|
|
21
|
+
total: 0,
|
|
22
|
+
completed: 0,
|
|
23
|
+
inProgress: 0,
|
|
24
|
+
pending: 0
|
|
25
|
+
};
|
|
26
|
+
let inTodos = false;
|
|
27
|
+
for (const raw of m[1].split("\n")) {
|
|
28
|
+
const line = raw.replace(/\r$/, "");
|
|
29
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
30
|
+
const indent = line.length - line.trimStart().length;
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (indent === 0) {
|
|
33
|
+
inTodos = false;
|
|
34
|
+
const [key, ...rest] = trimmed.split(":");
|
|
35
|
+
const value = rest.join(":").trim();
|
|
36
|
+
if (key === "name") fm.name = unquote(value);
|
|
37
|
+
else if (key === "status") {
|
|
38
|
+
const v = unquote(value);
|
|
39
|
+
if (v !== "") fm.status = v;
|
|
40
|
+
} else if (key === "todos") inTodos = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inTodos) {
|
|
44
|
+
const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed);
|
|
45
|
+
if (sm) {
|
|
46
|
+
const status = unquote(sm[1].trim());
|
|
47
|
+
fm.total++;
|
|
48
|
+
if (status === "completed") fm.completed++;
|
|
49
|
+
else if (status === "in_progress") fm.inProgress++;
|
|
50
|
+
else if (status === "pending") fm.pending++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return fm;
|
|
55
|
+
}
|
|
56
|
+
function unquote(v) {
|
|
57
|
+
return v.replace(/^["']|["']$/g, "");
|
|
58
|
+
}
|
|
59
|
+
function nextLead(text) {
|
|
60
|
+
const lines = text.split("\n");
|
|
61
|
+
let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()));
|
|
62
|
+
if (i === -1) return "";
|
|
63
|
+
for (i += 1; i < lines.length; i++) {
|
|
64
|
+
const l = lines[i].replace(/\r$/, "").trim();
|
|
65
|
+
if (l === "") continue;
|
|
66
|
+
if (l.startsWith("#")) return "";
|
|
67
|
+
const cleaned = l.replace(/^[-*]\s+/, "").replace(/\*\*/g, "").trim();
|
|
68
|
+
return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned;
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
function discoverPlanFiles(root) {
|
|
73
|
+
const dir = join(root, ".agents", "plans");
|
|
74
|
+
if (!existsSync(dir)) return [];
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".plan.md")).map((e) => e.name).sort();
|
|
82
|
+
}
|
|
83
|
+
function collectPlans(root) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const name of discoverPlanFiles(root)) {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = readFileSync(join(root, ".agents", "plans", name), "utf8");
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const fm = parsePlanFrontmatter(text);
|
|
93
|
+
if (!fm) continue;
|
|
94
|
+
out.push({
|
|
95
|
+
cr: name.replace(/\.plan\.md$/, ""),
|
|
96
|
+
name: fm.name,
|
|
97
|
+
total: fm.total,
|
|
98
|
+
completed: fm.completed,
|
|
99
|
+
inProgress: fm.inProgress,
|
|
100
|
+
status: fm.status,
|
|
101
|
+
next: nextLead(text)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return out.sort((a, b) => a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0);
|
|
105
|
+
}
|
|
106
|
+
function filterByStatus(plans, status) {
|
|
107
|
+
return plans.filter((p) => p.status === status);
|
|
108
|
+
}
|
|
109
|
+
const COLUMNS = [
|
|
110
|
+
"cr",
|
|
111
|
+
"name",
|
|
112
|
+
"total",
|
|
113
|
+
"completed",
|
|
114
|
+
"inProgress",
|
|
115
|
+
"status",
|
|
116
|
+
"next"
|
|
117
|
+
];
|
|
118
|
+
function toonField(v) {
|
|
119
|
+
if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
120
|
+
return v;
|
|
121
|
+
}
|
|
122
|
+
function toToon(plans) {
|
|
123
|
+
return [`plans[${plans.length}]{${COLUMNS.join(",")}}:`, ...plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(",")}`)].join("\n");
|
|
124
|
+
}
|
|
125
|
+
function main(argv) {
|
|
126
|
+
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
|
|
127
|
+
const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
|
|
128
|
+
const statusFilter = argv.includes("--status") ? argv[argv.indexOf("--status") + 1] : void 0;
|
|
129
|
+
let plans = collectPlans(root);
|
|
130
|
+
if (statusFilter !== void 0) plans = filterByStatus(plans, statusFilter);
|
|
131
|
+
const out = format === "json" ? JSON.stringify(plans, null, 2) : toToon(plans);
|
|
132
|
+
process.stdout.write(`${out}\n`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
136
|
+
//#endregion
|
|
137
|
+
export { DEFAULT_PLAN_STATUS, TODO_STATUSES, collectPlans, filterByStatus, main, nextLead, parsePlanFrontmatter, toToon };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# discover-specs
|
|
2
2
|
|
|
3
|
-
The concrete engine for SDD **spec discovery**. A non-user-invocable skill carrying a self-contained `.
|
|
3
|
+
The concrete engine for SDD **spec discovery**. A non-user-invocable skill carrying a self-contained `.mjs`
|
|
4
4
|
script that scans the three fixed SDD spec locations — plus any opt-in extra anchors declared in
|
|
5
5
|
`.agents/sdd/spec-anchors.toml` (ADR-0019, curated via `manage-spec-anchors`) — filters candidates by
|
|
6
6
|
the lifecycle `status` shape, parses each `spec.md`'s frontmatter only, and emits a TOON list of the
|
|
7
7
|
specs found.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/discover-specs.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/discover-specs.mjs`](./scripts/discover-specs.mjs)
|
|
11
|
+
- **Tests:** [`src/discover-specs/discover-specs.test.mts`](../../src/discover-specs/discover-specs.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
node scripts/discover-specs.
|
|
14
|
+
node scripts/discover-specs.mjs --root . --format toon
|
|
15
15
|
```
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
The concrete engine for SDD **spec discovery**. It locates the
|
|
12
12
|
project specs in a repo and returns each one's frontmatter — **without reading any spec body** — so a
|
|
13
13
|
consumer (the **gateway**, corpus tooling) can route on `status` / `project-path` / `approval`
|
|
14
|
-
cheaply. It carries a self-contained `.
|
|
14
|
+
cheaply. It carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
15
15
|
|
|
16
16
|
## Recognition — location-bounded and shape-confirmed
|
|
17
17
|
|
|
@@ -37,7 +37,7 @@ A `spec.md` is a spec only when **both** hold (ADR-0017, narrowed; extra anchors
|
|
|
37
37
|
## Run the scan
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
node "<skill>/scripts/discover-specs.
|
|
40
|
+
node "<skill>/scripts/discover-specs.mjs" [--root .] [--format toon|json] [--resolve <name>]
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
- Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/discover-specs/discover-specs.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/discover-specs/scripts/discover-specs.mjs","commands":{}};
|
|
1
4
|
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
5
|
import { join } from "node:path";
|
|
3
6
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
//#region
|
|
7
|
+
//#region src/discover-specs/discover-specs.mts
|
|
5
8
|
const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
|
|
6
9
|
"draft",
|
|
7
10
|
"approved",
|
|
@@ -58,11 +58,11 @@ recorded as distilled).
|
|
|
58
58
|
**Determine distilled-ness by a structured JSONL parse, never a substring or regex text match.**
|
|
59
59
|
The ledger shards are JSONL (one JSON object per line). **Reuse the existing parse contract** rather
|
|
60
60
|
than hand-rolling one: run the exported **`distilledCrRefs`** engine from
|
|
61
|
-
`../plan-retirement/scripts/retire-plans.
|
|
61
|
+
`../plan-retirement/scripts/retire-plans.mjs` (its `--distilled` flag) over the project `ledger/` directory and membership-test
|
|
62
62
|
the mission's `<cr-ref>` against the set it returns, e.g.
|
|
63
63
|
|
|
64
64
|
```
|
|
65
|
-
node
|
|
65
|
+
node "<plan-retirement skill>/scripts/retire-plans.mjs" --distilled <ledger-dir>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`distilledCrRefs` parses **each line** with `JSON.parse` (keying only on `kind === "strategy"` with a
|
|
@@ -58,6 +58,16 @@ misplaced only when it **neither** follows the declared strategy **nor** matches
|
|
|
58
58
|
— judging on the strategy alone raises false findings against placements a human already adjudicated. A declared layout settles *where a node goes*; it never licenses a capability to
|
|
59
59
|
scatter across nodes. (Strategy is policy — read it; homes are data — derive them.)
|
|
60
60
|
|
|
61
|
+
**Depth on capability nodes; `design/` by its own shape** (`sdd:spec-structure-governance`). The
|
|
62
|
+
two-level cap covers capability nodes: one three levels deep is a depth finding naming it, its
|
|
63
|
+
sub-grouping owed to a `concept:` tag. `design/` is judged by its own shape instead — current design
|
|
64
|
+
at the top, one file per perspective, the `decisions/` ADR log, and at most one version folder
|
|
65
|
+
holding the next design. The placement map names that version folder and its cut trigger; the named
|
|
66
|
+
folder and its files raise no finding, any other `design/` sub-folder besides `decisions/` is a
|
|
67
|
+
perspective-folder finding, a named version with no cut trigger is a finding, and so is a named
|
|
68
|
+
version whose folder does not exist (a stale record the cut left behind). Tell the two
|
|
69
|
+
apart by the map, never by the folder's contents.
|
|
70
|
+
|
|
61
71
|
## The intra-spec structural acts
|
|
62
72
|
|
|
63
73
|
It acts on each spec's **structure**, not its content — one project is **one spec**, so structural
|
|
@@ -13,8 +13,8 @@ conductor's autonomy bar (baked into `start-mission`), a hard floor + a three-di
|
|
|
13
13
|
|
|
14
14
|
## Legal-state tuples
|
|
15
15
|
|
|
16
|
-
The mechanical authority is `spec-gate/scripts/check-spec-state.
|
|
17
|
-
(`node <skill>/scripts/check-spec-state.
|
|
16
|
+
The mechanical authority is `spec-gate/scripts/check-spec-state.mjs` — run it
|
|
17
|
+
(`node <skill>/scripts/check-spec-state.mjs`) to enforce; if `node` is unavailable, apply the same
|
|
18
18
|
rules by reading frontmatter. The `(status, markers, .feature, approval)` tuple of the root
|
|
19
19
|
`spec.md` — plus the durable gate lines globbed from its sibling `ledger/` shards (a legacy `ledger.jsonl` still counts) — is **illegal** when:
|
|
20
20
|
|
package/skills/init/README.md
CHANGED
|
@@ -7,13 +7,13 @@ mission runs. A user-invocable setup skill (sibling to `start-mission` / `manage
|
|
|
7
7
|
and invokes no gate.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/wire-statusline.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/wire-statusline.mjs`](./scripts/wire-statusline.mjs)
|
|
11
|
+
- **Tests:** [`src/init/wire-statusline.test.mts`](../../src/init/wire-statusline.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
node scripts/wire-statusline.
|
|
15
|
-
node scripts/wire-statusline.
|
|
16
|
-
node scripts/wire-statusline.
|
|
14
|
+
node scripts/wire-statusline.mjs --root . --detect
|
|
15
|
+
node scripts/wire-statusline.mjs --root . --wire --mode own-line
|
|
16
|
+
node scripts/wire-statusline.mjs --root . --wire --mode same-line # --no-global-base to shadow deliberately
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The engine writes only project `.claude/settings.json` (never the global settings file) and, in a
|
package/skills/init/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ On enable, ask whether the mission status should render:
|
|
|
29
29
|
Before wiring, run the read-only detection — Claude Code's project `statusLine` **replaces** the global one (no merge), so a fresh project wiring would silently shadow a status line living in `~/.claude/settings.json`:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
node "<skill>/scripts/wire-statusline.
|
|
32
|
+
node "<skill>/scripts/wire-statusline.mjs" --root . --detect
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
It reports the project statusLine (absent | wired | foreign), the global statusLine (present | absent), and the shadow risk. On a shadow risk (project absent, global present), tell the user wiring will shadow their global status line and ask:
|
|
@@ -44,8 +44,8 @@ No shadow risk → proceed straight to wiring.
|
|
|
44
44
|
Run the engine to compose the reader into project settings and (in a git repo) gitignore the status file:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
node "<skill>/scripts/wire-statusline.
|
|
48
|
-
node "<skill>/scripts/wire-statusline.
|
|
47
|
+
node "<skill>/scripts/wire-statusline.mjs" --root . --wire --mode own-line
|
|
48
|
+
node "<skill>/scripts/wire-statusline.mjs" --root . --wire --mode same-line # add --no-global-base to shadow deliberately
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
The engine:
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/init/wire-statusline.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/init/scripts/wire-statusline.mjs","commands":{}};
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/init/wire-statusline.mts
|
|
9
|
+
const STATUS_FILE = ".agents/sdd/statusline";
|
|
10
|
+
const SETTINGS_FILE = ".claude/settings.json";
|
|
11
|
+
const GITIGNORE_FILE = ".gitignore";
|
|
12
|
+
const GLOBAL_SETTINGS_FILE = join(homedir(), ".claude", "settings.json");
|
|
13
|
+
const MARKER_BEGIN = "# sdd-statusline:begin";
|
|
14
|
+
const MARKER_END = "# sdd-statusline:end";
|
|
15
|
+
const MODE_PREFIX = "# sdd-statusline:mode:";
|
|
16
|
+
const ORIG_OPEN = "__sdd_orig() {";
|
|
17
|
+
const ORIG_CLOSE = "}";
|
|
18
|
+
const NO_BASE = ":";
|
|
19
|
+
function buildCommand(base, mode) {
|
|
20
|
+
const origBody = base && base.trim() !== "" ? base : NO_BASE;
|
|
21
|
+
const render = mode === "own-line" ? `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s\\n%s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi` : `if [ -s "$__sdd_file" ]; then\n __sdd_val="$(cat "$__sdd_file")"\n printf '%s | %s\\n' "$__sdd_base" "$__sdd_val"\nelse\n printf '%s\\n' "$__sdd_base"\nfi`;
|
|
22
|
+
return [
|
|
23
|
+
MARKER_BEGIN,
|
|
24
|
+
`${MODE_PREFIX}${mode}`,
|
|
25
|
+
ORIG_OPEN,
|
|
26
|
+
origBody,
|
|
27
|
+
ORIG_CLOSE,
|
|
28
|
+
"__sdd_base=\"$(__sdd_orig 2>/dev/null)\"",
|
|
29
|
+
`__sdd_file="${STATUS_FILE}"`,
|
|
30
|
+
render,
|
|
31
|
+
MARKER_END
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
function parseCommand(command) {
|
|
35
|
+
if (!command?.includes(MARKER_BEGIN)) return { wired: false };
|
|
36
|
+
const modeMatch = new RegExp(`${MODE_PREFIX}(own-line|same-line)`).exec(command);
|
|
37
|
+
const origMatch = /__sdd_orig\(\) \{\n([\s\S]*?)\n\}/.exec(command);
|
|
38
|
+
const mode = modeMatch?.[1] ?? "own-line";
|
|
39
|
+
const origBody = origMatch?.[1] ?? NO_BASE;
|
|
40
|
+
return {
|
|
41
|
+
wired: true,
|
|
42
|
+
base: origBody === NO_BASE ? void 0 : origBody,
|
|
43
|
+
mode
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function baseToWrap(existingCommand) {
|
|
47
|
+
const parsed = parseCommand(existingCommand);
|
|
48
|
+
return parsed.wired ? parsed.base : existingCommand;
|
|
49
|
+
}
|
|
50
|
+
function readSettingsFile(file) {
|
|
51
|
+
if (!existsSync(file)) return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readSettings(root) {
|
|
59
|
+
return readSettingsFile(join(root, SETTINGS_FILE));
|
|
60
|
+
}
|
|
61
|
+
function readStatusLineCommand(file) {
|
|
62
|
+
const command = readSettingsFile(file).statusLine?.command;
|
|
63
|
+
if (typeof command !== "string" || command.trim() === "") return void 0;
|
|
64
|
+
return command;
|
|
65
|
+
}
|
|
66
|
+
function writeSettings(root, settings) {
|
|
67
|
+
const file = join(root, SETTINGS_FILE);
|
|
68
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
69
|
+
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
function composeStatusLine(root, mode, globalCommand) {
|
|
72
|
+
const settings = readSettings(root);
|
|
73
|
+
const existing = settings.statusLine?.command;
|
|
74
|
+
const hasProjectStatusLine = typeof existing === "string" && existing.trim() !== "";
|
|
75
|
+
const globalBaseComposed = !hasProjectStatusLine && globalCommand !== void 0;
|
|
76
|
+
const command = buildCommand(hasProjectStatusLine ? baseToWrap(existing) : globalCommand, mode);
|
|
77
|
+
if (existing === command) return {
|
|
78
|
+
changed: false,
|
|
79
|
+
command,
|
|
80
|
+
globalBaseComposed: false
|
|
81
|
+
};
|
|
82
|
+
settings.statusLine = {
|
|
83
|
+
...settings.statusLine,
|
|
84
|
+
type: "command",
|
|
85
|
+
command
|
|
86
|
+
};
|
|
87
|
+
writeSettings(root, settings);
|
|
88
|
+
return {
|
|
89
|
+
changed: true,
|
|
90
|
+
command,
|
|
91
|
+
globalBaseComposed
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isGitRepo(root) {
|
|
95
|
+
return existsSync(join(root, ".git"));
|
|
96
|
+
}
|
|
97
|
+
function splitLines(text) {
|
|
98
|
+
const lines = text.split("\n");
|
|
99
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
function addGitignoreEntry(root) {
|
|
103
|
+
const file = join(root, GITIGNORE_FILE);
|
|
104
|
+
const lines = existsSync(file) ? splitLines(readFileSync(file, "utf8")) : [];
|
|
105
|
+
if (lines.some((l) => l.trim() === ".agents/sdd/statusline")) return { changed: false };
|
|
106
|
+
const next = [...lines, STATUS_FILE];
|
|
107
|
+
writeFileSync(file, `${next.join("\n")}\n`);
|
|
108
|
+
return { changed: true };
|
|
109
|
+
}
|
|
110
|
+
function detectStatusLines(root, globalSettingsFile) {
|
|
111
|
+
const projectCommand = readStatusLineCommand(join(root, SETTINGS_FILE));
|
|
112
|
+
const project = projectCommand === void 0 ? "absent" : parseCommand(projectCommand).wired ? "wired" : "foreign";
|
|
113
|
+
const global = readStatusLineCommand(globalSettingsFile ?? GLOBAL_SETTINGS_FILE) !== void 0 ? "present" : "absent";
|
|
114
|
+
return {
|
|
115
|
+
project,
|
|
116
|
+
global,
|
|
117
|
+
shadow: project === "absent" && global === "present"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function wireStatusline(root, mode, opts) {
|
|
121
|
+
return {
|
|
122
|
+
settings: composeStatusLine(root, mode, opts?.globalBase ?? true ? readStatusLineCommand(opts?.globalSettingsFile ?? GLOBAL_SETTINGS_FILE) : void 0),
|
|
123
|
+
gitignore: isGitRepo(root) ? {
|
|
124
|
+
...addGitignoreEntry(root),
|
|
125
|
+
skipped: false
|
|
126
|
+
} : {
|
|
127
|
+
changed: false,
|
|
128
|
+
skipped: true
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function flag(argv, name) {
|
|
133
|
+
const i = argv.indexOf(name);
|
|
134
|
+
return i === -1 ? void 0 : argv[i + 1];
|
|
135
|
+
}
|
|
136
|
+
function main(argv) {
|
|
137
|
+
const root = flag(argv, "--root") ?? ".";
|
|
138
|
+
const globalSettingsFile = flag(argv, "--global-settings");
|
|
139
|
+
const w = (s) => process.stdout.write(`${s}\n`);
|
|
140
|
+
if (argv.includes("--detect")) {
|
|
141
|
+
const result = detectStatusLines(root, globalSettingsFile);
|
|
142
|
+
w(`project statusLine: ${result.project}`);
|
|
143
|
+
w(`global statusLine: ${result.global}`);
|
|
144
|
+
w(result.shadow ? "shadow risk: yes — wiring would shadow the global statusline; compose it as the base (default) or pass --no-global-base to shadow deliberately" : "shadow risk: no");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (argv.includes("--wire")) {
|
|
148
|
+
const mode = flag(argv, "--mode");
|
|
149
|
+
if (mode !== "own-line" && mode !== "same-line") {
|
|
150
|
+
w("refused: --mode must be own-line or same-line");
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
const result = wireStatusline(root, mode, {
|
|
154
|
+
globalSettingsFile,
|
|
155
|
+
globalBase: !argv.includes("--no-global-base")
|
|
156
|
+
});
|
|
157
|
+
w(result.settings.changed ? `wired statusLine (${mode})` : "statusLine already wired (unchanged)");
|
|
158
|
+
if (result.settings.globalBaseComposed) w("composed the global statusLine command as the wrapped base");
|
|
159
|
+
if (result.gitignore.skipped) w("not a git repo — gitignore skipped");
|
|
160
|
+
else w(result.gitignore.changed ? `added ${STATUS_FILE} to .gitignore` : ".gitignore already up to date");
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
w("usage: wire-statusline --root <dir> --wire --mode own-line|same-line [--global-settings <file>] [--no-global-base]\n wire-statusline --root <dir> --detect [--global-settings <file>]");
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
167
|
+
//#endregion
|
|
168
|
+
export { GITIGNORE_FILE, GLOBAL_SETTINGS_FILE, SETTINGS_FILE, STATUS_FILE, addGitignoreEntry, buildCommand, composeStatusLine, detectStatusLines, isGitRepo, main, parseCommand, readStatusLineCommand, wireStatusline };
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
The concrete engine for `.agents/sdd/.sddignore` — the optional gitignore-syntax file
|
|
4
4
|
`resolve-tracking` reads to decide whether an artifact is tracked or ignored. A non-user-invocable
|
|
5
|
-
skill, loaded in-session by the `manage` gateway (Housekeeping), carrying a self-contained `.
|
|
5
|
+
skill, loaded in-session by the `manage` gateway (Housekeeping), carrying a self-contained `.mjs`
|
|
6
6
|
script that lists / adds / removes / induces / previews the ignore rules so users never hand-edit the
|
|
7
7
|
file. Order is meaningful (last-match-wins), so rules are never re-sorted.
|
|
8
8
|
|
|
9
9
|
- **Skill contract:** [`SKILL.md`](./SKILL.md)
|
|
10
|
-
- **Script:** [`scripts/manage-ignore.
|
|
11
|
-
- **Tests:** [`
|
|
10
|
+
- **Script:** [`scripts/manage-ignore.mjs`](./scripts/manage-ignore.mjs)
|
|
11
|
+
- **Tests:** [`src/manage-ignore/manage-ignore.test.mts`](../../src/manage-ignore/manage-ignore.test.mts) (`node:test`)
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
node scripts/manage-ignore.
|
|
15
|
-
node scripts/manage-ignore.
|
|
16
|
-
node scripts/manage-ignore.
|
|
17
|
-
node scripts/manage-ignore.
|
|
18
|
-
node scripts/manage-ignore.
|
|
14
|
+
node scripts/manage-ignore.mjs --list
|
|
15
|
+
node scripts/manage-ignore.mjs --induce build/output/app.log
|
|
16
|
+
node scripts/manage-ignore.mjs --preview '*.log'
|
|
17
|
+
node scripts/manage-ignore.mjs --preview '!keep.log'
|
|
18
|
+
node scripts/manage-ignore.mjs --add '*.log'
|
|
19
19
|
```
|
|
@@ -12,8 +12,7 @@ The concrete engine for `.agents/sdd/.sddignore` — the optional gitignore-synt
|
|
|
12
12
|
`resolve-tracking` consults to decide whether an artifact is **tracked** or **ignored**. It is the
|
|
13
13
|
**write** side of the ignore file (`resolve-tracking` is the read side). It exists so a user curates
|
|
14
14
|
the ignore rules through a clean interface instead of hand-editing the file. Loaded **in-session** by
|
|
15
|
-
the `manage` gateway (Housekeeping group); it carries a self-contained `.
|
|
16
|
-
node-≥23.6 / no-deps convention).
|
|
15
|
+
the `manage` gateway (Housekeeping group); it carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11).
|
|
17
16
|
|
|
18
17
|
## The file it curates
|
|
19
18
|
|
|
@@ -25,7 +24,7 @@ engine therefore **never re-sorts**: `--add` appends, `--remove` preserves the o
|
|
|
25
24
|
## Run an operation
|
|
26
25
|
|
|
27
26
|
```bash
|
|
28
|
-
node "<skill>/scripts/manage-ignore.
|
|
27
|
+
node "<skill>/scripts/manage-ignore.mjs" [--root .] <operation>
|
|
29
28
|
```
|
|
30
29
|
|
|
31
30
|
| Operation | Effect |
|