cyber-sdd 0.6.0 → 0.7.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.plugin/plugin.json +1 -1
- package/agents/sdd-automaton.md +2 -3
- package/agents/sdd-impl-judge.md +5 -4
- package/agents/sdd-scanner.md +1 -1
- package/agents/sdd-spec-judge.md +3 -3
- package/agents/sdd-warden.md +8 -0
- package/dist/check-project-specs/scripts/check-project-specs.js +1 -0
- package/package.json +2 -2
- package/skills/align-spec/README.md +3 -3
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.bundle.mjs +6753 -0
- package/skills/align-spec/scripts/align-spec.mjs +12 -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 +1 -1
- package/skills/blast-estimate/SKILL.md +1 -1
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +593 -0
- package/skills/blast-estimate/scripts/blast-estimate.mjs +12 -0
- package/skills/check-field-mandates/SKILL.md +1 -1
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +251 -0
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +12 -0
- package/skills/check-partition-quality/SKILL.md +1 -1
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +233 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +12 -0
- package/skills/check-plan-safety/README.md +3 -3
- package/skills/check-plan-safety/SKILL.md +1 -1
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +107 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +12 -0
- package/skills/check-plugin-manifests/SKILL.md +1 -1
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +248 -0
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +12 -0
- package/skills/check-project-specs/README.md +3 -3
- package/skills/check-project-specs/SKILL.md +2 -2
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +584 -0
- package/skills/check-project-specs/scripts/check-project-specs.mjs +12 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +1 -0
- package/skills/check-retired-terms/README.md +2 -2
- package/skills/check-retired-terms/SKILL.md +2 -2
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +184 -0
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +12 -0
- package/skills/check-scenario-overlap/README.md +2 -2
- package/skills/check-scenario-overlap/SKILL.md +2 -3
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +175 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +12 -0
- package/skills/check-spec-references/README.md +1 -1
- package/skills/check-spec-references/SKILL.md +1 -1
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +285 -0
- package/skills/check-spec-references/scripts/check-spec-references.mjs +12 -0
- package/skills/check-spec-structure/README.md +2 -2
- package/skills/check-spec-structure/SKILL.md +1 -1
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +241 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +12 -0
- package/skills/collision-ladder/SKILL.md +3 -3
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +7041 -0
- package/skills/collision-ladder/scripts/collision-ladder.mjs +12 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +4 -2
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/README.md +2 -2
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.bundle.mjs +181 -0
- package/skills/concept-index/scripts/concept-index.mjs +12 -0
- package/skills/discover-plans/README.md +2 -2
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +135 -0
- package/skills/discover-plans/scripts/discover-plans.mjs +12 -0
- package/skills/discover-specs/README.md +1 -1
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +319 -0
- package/skills/discover-specs/scripts/discover-specs.mjs +12 -0
- package/skills/doctrine-loop/SKILL.md +2 -2
- package/skills/formation-loop/SKILL.md +11 -4
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +3 -3
- package/skills/init/SKILL.md +3 -3
- package/skills/init/scripts/wire-statusline.bundle.mjs +166 -0
- package/skills/init/scripts/wire-statusline.mjs +12 -0
- package/skills/manage-ignore/README.md +5 -5
- package/skills/manage-ignore/SKILL.md +1 -1
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +244 -0
- package/skills/manage-ignore/scripts/manage-ignore.mjs +12 -0
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +237 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +12 -0
- package/skills/manage-spec-anchors/README.md +4 -4
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +308 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +12 -0
- package/skills/mission-graph/SKILL.md +9 -9
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +1038 -0
- package/skills/mission-graph/scripts/mission-graph.mjs +12 -0
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/README.md +1 -1
- package/skills/place-node/SKILL.md +1 -1
- package/skills/place-node/scripts/place-node.bundle.mjs +115 -0
- package/skills/place-node/scripts/place-node.mjs +12 -0
- package/skills/plan-retirement/SKILL.md +1 -1
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +121 -0
- package/skills/plan-retirement/scripts/retire-plans.mjs +12 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +6 -0
- package/skills/plugin-contract-governance/SKILL.md +5 -12
- package/skills/resolve-governances/README.md +1 -1
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +350 -0
- package/skills/resolve-governances/scripts/resolve-governances.mjs +12 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +1 -1
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +150 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +12 -0
- package/skills/scaffold-project-spec/SKILL.md +2 -2
- package/skills/sdd/SKILL.md +1 -1
- package/skills/solution-producer-governance/README.md +1 -1
- package/skills/solution-producer-governance/SKILL.md +1 -1
- package/skills/spec-gate/SKILL.md +5 -5
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +420 -0
- package/skills/spec-gate/scripts/check-spec-state.mjs +12 -0
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +6498 -0
- package/skills/spec-gate/scripts/check-suite.mjs +12 -0
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +6577 -0
- package/skills/spec-gate/scripts/classify-edit-class.mjs +12 -0
- package/skills/spec-producer-governance/README.md +1 -1
- package/skills/spec-producer-governance/SKILL.md +3 -3
- package/skills/spec-structure-governance/README.md +4 -2
- package/skills/spec-structure-governance/SKILL.md +42 -7
- package/skills/start-mission/README.md +1 -1
- package/skills/start-mission/SKILL.md +6 -6
- package/skills/suite-format-governance/references/rubric.md +1 -1
- package/skills/touch-set-correction/SKILL.md +2 -2
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +6655 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +12 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +12 -3
- package/skills/verify-scenarios/README.md +3 -3
- package/skills/verify-scenarios/SKILL.md +3 -3
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +6537 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +12 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
//#region skills/discover-specs/scripts/discover-specs.mts
|
|
6
|
+
const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
|
|
7
|
+
"draft",
|
|
8
|
+
"approved",
|
|
9
|
+
"implemented",
|
|
10
|
+
"deprecated"
|
|
11
|
+
]);
|
|
12
|
+
const ROOT_PROJECT_NAME = "repo";
|
|
13
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
"dist",
|
|
17
|
+
".turbo",
|
|
18
|
+
".next",
|
|
19
|
+
"coverage"
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
|
|
23
|
+
*
|
|
24
|
+
* A name blocklist cannot express this: it can only say "this is called X", never "this is a
|
|
25
|
+
* boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
|
|
26
|
+
* descending straight into the checkout that directory marks — one level off from where the guard
|
|
27
|
+
* was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
|
|
28
|
+
* found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
|
|
29
|
+
* guard silently auditing a tree nobody has.
|
|
30
|
+
*
|
|
31
|
+
* `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
|
|
32
|
+
*/
|
|
33
|
+
function isNestedCheckout(abs) {
|
|
34
|
+
return existsSync(join(abs, ".git"));
|
|
35
|
+
}
|
|
36
|
+
const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
|
|
37
|
+
function parseFrontmatter(text) {
|
|
38
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
39
|
+
if (!m) return null;
|
|
40
|
+
const fm = { approval: {} };
|
|
41
|
+
let gate = null;
|
|
42
|
+
for (const raw of m[1].split("\n")) {
|
|
43
|
+
const line = raw.replace(/\r$/, "");
|
|
44
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
45
|
+
const indent = line.length - line.trimStart().length;
|
|
46
|
+
const [key, ...rest] = line.trim().split(":");
|
|
47
|
+
const value = rest.join(":").trim();
|
|
48
|
+
if (indent === 0) {
|
|
49
|
+
gate = null;
|
|
50
|
+
if (key === "status") fm.status = unquote(value);
|
|
51
|
+
else if (key === "project-path") fm.projectPath = unquote(value);
|
|
52
|
+
else if (key === "name") fm.name = unquote(value);
|
|
53
|
+
else if (key === "approval") gate = null;
|
|
54
|
+
} else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
|
|
55
|
+
else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
|
|
56
|
+
}
|
|
57
|
+
return fm;
|
|
58
|
+
}
|
|
59
|
+
function insideApproval(block, line) {
|
|
60
|
+
const lines = block.split("\n");
|
|
61
|
+
const idx = lines.indexOf(line);
|
|
62
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
63
|
+
const l = lines[i].replace(/\r$/, "");
|
|
64
|
+
if (l.trim() === "" || l.trim().startsWith("#")) continue;
|
|
65
|
+
if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function unquote(v) {
|
|
70
|
+
return v.replace(/^["']|["']$/g, "");
|
|
71
|
+
}
|
|
72
|
+
function classifyLocation(relPath) {
|
|
73
|
+
const p = relPath.replace(/\\/g, "/");
|
|
74
|
+
if (p === ".agents/spec/spec.md") return {
|
|
75
|
+
pattern: "root-single",
|
|
76
|
+
locationDir: ""
|
|
77
|
+
};
|
|
78
|
+
const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
|
|
79
|
+
if (multi) return {
|
|
80
|
+
pattern: "root-multi",
|
|
81
|
+
locationDir: multi[1]
|
|
82
|
+
};
|
|
83
|
+
const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
|
|
84
|
+
if (nested) return {
|
|
85
|
+
pattern: "nested",
|
|
86
|
+
locationDir: nested[1]
|
|
87
|
+
};
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function deriveName(loc, fm) {
|
|
91
|
+
if (fm.name) return {
|
|
92
|
+
name: fm.name,
|
|
93
|
+
nameSource: "declared"
|
|
94
|
+
};
|
|
95
|
+
if (loc.pattern === "root-single") return {
|
|
96
|
+
name: ROOT_PROJECT_NAME,
|
|
97
|
+
nameSource: "derived"
|
|
98
|
+
};
|
|
99
|
+
if (loc.pattern === "root-multi") return {
|
|
100
|
+
name: loc.locationDir,
|
|
101
|
+
nameSource: "derived"
|
|
102
|
+
};
|
|
103
|
+
if (loc.pattern === "extra" && loc.capturedName) return {
|
|
104
|
+
name: loc.capturedName,
|
|
105
|
+
nameSource: "derived"
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
name: loc.locationDir.split("/").pop() ?? loc.locationDir,
|
|
109
|
+
nameSource: "guessed"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function discoverSpecFiles(root) {
|
|
113
|
+
const found = [];
|
|
114
|
+
const walk = (relDir) => {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = readdirSync(join(root, relDir), { withFileTypes: true });
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
123
|
+
const childRel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
124
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
125
|
+
if (e.name === ".agents") {
|
|
126
|
+
probeAgents(root, childRel, found);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
walk(childRel);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
walk("");
|
|
133
|
+
return found;
|
|
134
|
+
}
|
|
135
|
+
function probeAgents(root, agentsRel, found) {
|
|
136
|
+
const single = `${agentsRel}/spec/spec.md`;
|
|
137
|
+
if (existsSync(join(root, single))) found.push(single);
|
|
138
|
+
if (agentsRel === ".agents") {
|
|
139
|
+
const specsDir = join(root, ".agents", "specs");
|
|
140
|
+
let projects;
|
|
141
|
+
try {
|
|
142
|
+
projects = readdirSync(specsDir, { withFileTypes: true });
|
|
143
|
+
} catch {
|
|
144
|
+
projects = [];
|
|
145
|
+
}
|
|
146
|
+
for (const p of projects) {
|
|
147
|
+
if (!p.isDirectory()) continue;
|
|
148
|
+
const rel = `.agents/specs/${p.name}/spec.md`;
|
|
149
|
+
if (existsSync(join(root, rel))) found.push(rel);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseAnchorsToml(text) {
|
|
154
|
+
const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
155
|
+
if (!m) return [];
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
|
|
158
|
+
return out.filter((s) => s !== "");
|
|
159
|
+
}
|
|
160
|
+
function readAnchors(root) {
|
|
161
|
+
const file = join(root, ANCHORS_CONFIG);
|
|
162
|
+
if (!existsSync(file)) return [];
|
|
163
|
+
try {
|
|
164
|
+
return parseAnchorsToml(readFileSync(file, "utf8"));
|
|
165
|
+
} catch {
|
|
166
|
+
process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function collectDescendants(root, startDir) {
|
|
171
|
+
const out = [startDir];
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = readdirSync(join(root, startDir), { withFileTypes: true });
|
|
175
|
+
} catch {
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
for (const e of entries) {
|
|
179
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
180
|
+
const childRel = startDir ? `${startDir}/${e.name}` : e.name;
|
|
181
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
182
|
+
out.push(...collectDescendants(root, childRel));
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
function expandAnchor(root, pattern) {
|
|
187
|
+
const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
188
|
+
let frontier = [{ dir: "" }];
|
|
189
|
+
for (const seg of segs) {
|
|
190
|
+
const next = [];
|
|
191
|
+
if (seg === "**") {
|
|
192
|
+
for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
|
|
193
|
+
dir,
|
|
194
|
+
capturedName: node.capturedName
|
|
195
|
+
});
|
|
196
|
+
frontier = next;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const isGlob = seg === "*" || seg === "<project>";
|
|
200
|
+
for (const node of frontier) if (isGlob) {
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = readdirSync(join(root, node.dir), { withFileTypes: true });
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
for (const e of entries) {
|
|
208
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
209
|
+
const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
|
|
210
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
211
|
+
next.push({
|
|
212
|
+
dir: childRel,
|
|
213
|
+
capturedName: seg === "<project>" ? e.name : node.capturedName
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
} else next.push({
|
|
217
|
+
dir: node.dir ? `${node.dir}/${seg}` : seg,
|
|
218
|
+
capturedName: node.capturedName
|
|
219
|
+
});
|
|
220
|
+
frontier = next;
|
|
221
|
+
}
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const node of frontier) {
|
|
224
|
+
const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
|
|
225
|
+
if (existsSync(join(root, rel))) out.push({
|
|
226
|
+
rel,
|
|
227
|
+
capturedName: node.capturedName
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function recordFor(root, rel, loc) {
|
|
233
|
+
let fm;
|
|
234
|
+
try {
|
|
235
|
+
fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
|
|
240
|
+
const { name, nameSource } = deriveName(loc, fm);
|
|
241
|
+
return {
|
|
242
|
+
path: rel.replace(/\/spec\.md$/, ""),
|
|
243
|
+
name,
|
|
244
|
+
nameSource,
|
|
245
|
+
status: fm.status,
|
|
246
|
+
projectPath: fm.projectPath ?? "",
|
|
247
|
+
approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function collectSpecs(root) {
|
|
251
|
+
const out = [];
|
|
252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
253
|
+
for (const rel of discoverSpecFiles(root)) {
|
|
254
|
+
const loc = classifyLocation(rel);
|
|
255
|
+
if (!loc) continue;
|
|
256
|
+
const rec = recordFor(root, rel, loc);
|
|
257
|
+
if (rec) {
|
|
258
|
+
out.push(rec);
|
|
259
|
+
seen.add(rel);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
|
|
263
|
+
if (seen.has(rel)) continue;
|
|
264
|
+
const rec = recordFor(root, rel, {
|
|
265
|
+
pattern: "extra",
|
|
266
|
+
locationDir: rel.replace(/\/spec\.md$/, ""),
|
|
267
|
+
capturedName
|
|
268
|
+
});
|
|
269
|
+
if (rec) {
|
|
270
|
+
out.push(rec);
|
|
271
|
+
seen.add(rel);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
275
|
+
}
|
|
276
|
+
function resolveByName(specs, name) {
|
|
277
|
+
const want = name.trim().toLowerCase();
|
|
278
|
+
const hits = specs.filter((s) => s.name.toLowerCase() === want);
|
|
279
|
+
if (hits.length === 0) return { kind: "none" };
|
|
280
|
+
if (hits.length === 1) return {
|
|
281
|
+
kind: "match",
|
|
282
|
+
spec: hits[0]
|
|
283
|
+
};
|
|
284
|
+
return {
|
|
285
|
+
kind: "ambiguous",
|
|
286
|
+
candidates: hits
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const COLUMNS = [
|
|
290
|
+
"path",
|
|
291
|
+
"name",
|
|
292
|
+
"nameSource",
|
|
293
|
+
"status",
|
|
294
|
+
"projectPath",
|
|
295
|
+
"approvals"
|
|
296
|
+
];
|
|
297
|
+
function toonField(v) {
|
|
298
|
+
if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
299
|
+
return v;
|
|
300
|
+
}
|
|
301
|
+
function toToon(specs) {
|
|
302
|
+
return [`specs[${specs.length}]{${COLUMNS.join(",")}}:`, ...specs.map((s) => ` ${COLUMNS.map((c) => toonField(s[c])).join(",")}`)].join("\n");
|
|
303
|
+
}
|
|
304
|
+
function main(argv) {
|
|
305
|
+
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
|
|
306
|
+
const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
|
|
307
|
+
let specs = collectSpecs(root);
|
|
308
|
+
if (argv.includes("--resolve")) {
|
|
309
|
+
const name = argv[argv.indexOf("--resolve") + 1] ?? "";
|
|
310
|
+
const r = resolveByName(specs, name);
|
|
311
|
+
specs = r.kind === "match" ? [r.spec] : r.kind === "ambiguous" ? r.candidates : [];
|
|
312
|
+
}
|
|
313
|
+
const out = format === "json" ? JSON.stringify(specs, null, 2) : toToon(specs);
|
|
314
|
+
process.stdout.write(`${out}\n`);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
318
|
+
//#endregion
|
|
319
|
+
export { LIFECYCLE_STATUSES, classifyLocation, collectSpecs, deriveName, discoverSpecFiles, expandAnchor, main, parseAnchorsToml, parseFrontmatter, readAnchors, resolveByName, toToon };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
|
|
2
|
+
// A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
|
|
3
|
+
// will not strip types, so it runs the self-contained bundle built for the package.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const self = fileURLToPath(import.meta.url)
|
|
8
|
+
const installed = /[\\/]node_modules[\\/]/.test(self)
|
|
9
|
+
const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
|
|
10
|
+
const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
11
|
+
if (run.error) throw run.error
|
|
12
|
+
process.exitCode = run.status ?? 1
|
|
@@ -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.mts` over the project `ledger/` directory and membership-test
|
|
61
|
+
`../plan-retirement/scripts/retire-plans.mts` (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
|
|
@@ -91,10 +101,7 @@ shares one boundary.
|
|
|
91
101
|
axis is wrong), not a granularity split to carve as-proposed.
|
|
92
102
|
|
|
93
103
|
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?"
|
|
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.)
|
|
104
|
+
big" into "too big **along which axis** — and is that axis real?"
|
|
98
105
|
|
|
99
106
|
Alongside its findings a pass surfaces an **advisory layout-quality signal** — the scheduler's
|
|
100
107
|
**false-conflict rate** doubles as a **partition-quality metric**: a layout that keeps node↔folder
|
|
@@ -14,7 +14,7 @@ conductor's autonomy bar (baked into `start-mission`), a hard floor + a three-di
|
|
|
14
14
|
## Legal-state tuples
|
|
15
15
|
|
|
16
16
|
The mechanical authority is `spec-gate/scripts/check-spec-state.mts` — run it
|
|
17
|
-
(`node <skill>/scripts/check-spec-state.
|
|
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
|
@@ -11,9 +11,9 @@ and invokes no gate.
|
|
|
11
11
|
- **Tests:** [`scripts/wire-statusline.test.mts`](./scripts/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,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
//#region skills/init/scripts/wire-statusline.mts
|
|
7
|
+
const STATUS_FILE = ".agents/sdd/statusline";
|
|
8
|
+
const SETTINGS_FILE = ".claude/settings.json";
|
|
9
|
+
const GITIGNORE_FILE = ".gitignore";
|
|
10
|
+
const GLOBAL_SETTINGS_FILE = join(homedir(), ".claude", "settings.json");
|
|
11
|
+
const MARKER_BEGIN = "# sdd-statusline:begin";
|
|
12
|
+
const MARKER_END = "# sdd-statusline:end";
|
|
13
|
+
const MODE_PREFIX = "# sdd-statusline:mode:";
|
|
14
|
+
const ORIG_OPEN = "__sdd_orig() {";
|
|
15
|
+
const ORIG_CLOSE = "}";
|
|
16
|
+
const NO_BASE = ":";
|
|
17
|
+
function buildCommand(base, mode) {
|
|
18
|
+
const origBody = base && base.trim() !== "" ? base : NO_BASE;
|
|
19
|
+
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`;
|
|
20
|
+
return [
|
|
21
|
+
MARKER_BEGIN,
|
|
22
|
+
`${MODE_PREFIX}${mode}`,
|
|
23
|
+
ORIG_OPEN,
|
|
24
|
+
origBody,
|
|
25
|
+
ORIG_CLOSE,
|
|
26
|
+
"__sdd_base=\"$(__sdd_orig 2>/dev/null)\"",
|
|
27
|
+
`__sdd_file="${STATUS_FILE}"`,
|
|
28
|
+
render,
|
|
29
|
+
MARKER_END
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
32
|
+
function parseCommand(command) {
|
|
33
|
+
if (!command?.includes(MARKER_BEGIN)) return { wired: false };
|
|
34
|
+
const modeMatch = new RegExp(`${MODE_PREFIX}(own-line|same-line)`).exec(command);
|
|
35
|
+
const origMatch = /__sdd_orig\(\) \{\n([\s\S]*?)\n\}/.exec(command);
|
|
36
|
+
const mode = modeMatch?.[1] ?? "own-line";
|
|
37
|
+
const origBody = origMatch?.[1] ?? NO_BASE;
|
|
38
|
+
return {
|
|
39
|
+
wired: true,
|
|
40
|
+
base: origBody === NO_BASE ? void 0 : origBody,
|
|
41
|
+
mode
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function baseToWrap(existingCommand) {
|
|
45
|
+
const parsed = parseCommand(existingCommand);
|
|
46
|
+
return parsed.wired ? parsed.base : existingCommand;
|
|
47
|
+
}
|
|
48
|
+
function readSettingsFile(file) {
|
|
49
|
+
if (!existsSync(file)) return {};
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readSettings(root) {
|
|
57
|
+
return readSettingsFile(join(root, SETTINGS_FILE));
|
|
58
|
+
}
|
|
59
|
+
function readStatusLineCommand(file) {
|
|
60
|
+
const command = readSettingsFile(file).statusLine?.command;
|
|
61
|
+
if (typeof command !== "string" || command.trim() === "") return void 0;
|
|
62
|
+
return command;
|
|
63
|
+
}
|
|
64
|
+
function writeSettings(root, settings) {
|
|
65
|
+
const file = join(root, SETTINGS_FILE);
|
|
66
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
67
|
+
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`);
|
|
68
|
+
}
|
|
69
|
+
function composeStatusLine(root, mode, globalCommand) {
|
|
70
|
+
const settings = readSettings(root);
|
|
71
|
+
const existing = settings.statusLine?.command;
|
|
72
|
+
const hasProjectStatusLine = typeof existing === "string" && existing.trim() !== "";
|
|
73
|
+
const globalBaseComposed = !hasProjectStatusLine && globalCommand !== void 0;
|
|
74
|
+
const command = buildCommand(hasProjectStatusLine ? baseToWrap(existing) : globalCommand, mode);
|
|
75
|
+
if (existing === command) return {
|
|
76
|
+
changed: false,
|
|
77
|
+
command,
|
|
78
|
+
globalBaseComposed: false
|
|
79
|
+
};
|
|
80
|
+
settings.statusLine = {
|
|
81
|
+
...settings.statusLine,
|
|
82
|
+
type: "command",
|
|
83
|
+
command
|
|
84
|
+
};
|
|
85
|
+
writeSettings(root, settings);
|
|
86
|
+
return {
|
|
87
|
+
changed: true,
|
|
88
|
+
command,
|
|
89
|
+
globalBaseComposed
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function isGitRepo(root) {
|
|
93
|
+
return existsSync(join(root, ".git"));
|
|
94
|
+
}
|
|
95
|
+
function splitLines(text) {
|
|
96
|
+
const lines = text.split("\n");
|
|
97
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
function addGitignoreEntry(root) {
|
|
101
|
+
const file = join(root, GITIGNORE_FILE);
|
|
102
|
+
const lines = existsSync(file) ? splitLines(readFileSync(file, "utf8")) : [];
|
|
103
|
+
if (lines.some((l) => l.trim() === ".agents/sdd/statusline")) return { changed: false };
|
|
104
|
+
const next = [...lines, STATUS_FILE];
|
|
105
|
+
writeFileSync(file, `${next.join("\n")}\n`);
|
|
106
|
+
return { changed: true };
|
|
107
|
+
}
|
|
108
|
+
function detectStatusLines(root, globalSettingsFile) {
|
|
109
|
+
const projectCommand = readStatusLineCommand(join(root, SETTINGS_FILE));
|
|
110
|
+
const project = projectCommand === void 0 ? "absent" : parseCommand(projectCommand).wired ? "wired" : "foreign";
|
|
111
|
+
const global = readStatusLineCommand(globalSettingsFile ?? GLOBAL_SETTINGS_FILE) !== void 0 ? "present" : "absent";
|
|
112
|
+
return {
|
|
113
|
+
project,
|
|
114
|
+
global,
|
|
115
|
+
shadow: project === "absent" && global === "present"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function wireStatusline(root, mode, opts) {
|
|
119
|
+
return {
|
|
120
|
+
settings: composeStatusLine(root, mode, opts?.globalBase ?? true ? readStatusLineCommand(opts?.globalSettingsFile ?? GLOBAL_SETTINGS_FILE) : void 0),
|
|
121
|
+
gitignore: isGitRepo(root) ? {
|
|
122
|
+
...addGitignoreEntry(root),
|
|
123
|
+
skipped: false
|
|
124
|
+
} : {
|
|
125
|
+
changed: false,
|
|
126
|
+
skipped: true
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function flag(argv, name) {
|
|
131
|
+
const i = argv.indexOf(name);
|
|
132
|
+
return i === -1 ? void 0 : argv[i + 1];
|
|
133
|
+
}
|
|
134
|
+
function main(argv) {
|
|
135
|
+
const root = flag(argv, "--root") ?? ".";
|
|
136
|
+
const globalSettingsFile = flag(argv, "--global-settings");
|
|
137
|
+
const w = (s) => process.stdout.write(`${s}\n`);
|
|
138
|
+
if (argv.includes("--detect")) {
|
|
139
|
+
const result = detectStatusLines(root, globalSettingsFile);
|
|
140
|
+
w(`project statusLine: ${result.project}`);
|
|
141
|
+
w(`global statusLine: ${result.global}`);
|
|
142
|
+
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");
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (argv.includes("--wire")) {
|
|
146
|
+
const mode = flag(argv, "--mode");
|
|
147
|
+
if (mode !== "own-line" && mode !== "same-line") {
|
|
148
|
+
w("refused: --mode must be own-line or same-line");
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const result = wireStatusline(root, mode, {
|
|
152
|
+
globalSettingsFile,
|
|
153
|
+
globalBase: !argv.includes("--no-global-base")
|
|
154
|
+
});
|
|
155
|
+
w(result.settings.changed ? `wired statusLine (${mode})` : "statusLine already wired (unchanged)");
|
|
156
|
+
if (result.settings.globalBaseComposed) w("composed the global statusLine command as the wrapped base");
|
|
157
|
+
if (result.gitignore.skipped) w("not a git repo — gitignore skipped");
|
|
158
|
+
else w(result.gitignore.changed ? `added ${STATUS_FILE} to .gitignore` : ".gitignore already up to date");
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
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>]");
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
165
|
+
//#endregion
|
|
166
|
+
export { GITIGNORE_FILE, GLOBAL_SETTINGS_FILE, SETTINGS_FILE, STATUS_FILE, addGitignoreEntry, buildCommand, composeStatusLine, detectStatusLines, isGitRepo, main, parseCommand, readStatusLineCommand, wireStatusline };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
|
|
2
|
+
// A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
|
|
3
|
+
// will not strip types, so it runs the self-contained bundle built for the package.
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const self = fileURLToPath(import.meta.url)
|
|
8
|
+
const installed = /[\\/]node_modules[\\/]/.test(self)
|
|
9
|
+
const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
|
|
10
|
+
const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
11
|
+
if (run.error) throw run.error
|
|
12
|
+
process.exitCode = run.status ?? 1
|
|
@@ -11,9 +11,9 @@ file. Order is meaningful (last-match-wins), so rules are never re-sorted.
|
|
|
11
11
|
- **Tests:** [`scripts/manage-ignore.test.mts`](./scripts/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
|
```
|
|
@@ -25,7 +25,7 @@ engine therefore **never re-sorts**: `--add` appends, `--remove` preserves the o
|
|
|
25
25
|
## Run an operation
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
node "<skill>/scripts/manage-ignore.
|
|
28
|
+
node "<skill>/scripts/manage-ignore.mjs" [--root .] <operation>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
| Operation | Effect |
|