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
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// manage-scenario-bridge — the concrete curation engine for `<project-path>/.agents/sdd/scenario-bridge.toml`,
|
|
3
|
-
// the one-time per-project wiring the verify-scenarios bridge and the impl-judge step-0 consumption
|
|
4
|
-
// both read. It reads/writes ONLY that one project's scenario-bridge.toml — never a spec.md, status,
|
|
5
|
-
// approval, or a freeze; it never runs the bridge or a test.
|
|
6
|
-
//
|
|
7
|
-
// Operations:
|
|
8
|
-
// --list print every configured [[source]] block in order
|
|
9
|
-
// --scaffold --adapter <a> [--command <c>] --report-path <p>
|
|
10
|
-
// create the config with one source block; refused
|
|
11
|
-
// when the file already exists
|
|
12
|
-
// --add --adapter <a> [--command <c>] --report-path <p>
|
|
13
|
-
// append a source block to an existing config;
|
|
14
|
-
// refused when the file does not exist
|
|
15
|
-
//
|
|
16
|
-
// Every read/write is resolved beneath --project-path (a colocated project's project-path is its own
|
|
17
|
-
// repo root, so the config lands at the familiar .agents/sdd/scenario-bridge.toml there).
|
|
18
|
-
//
|
|
19
|
-
// Reuses the verify-scenarios bridge's own SourceConfig/parseSourcesToml shape — never diverge the
|
|
20
|
-
// format the reader (verify-scenarios) and the writer (this engine) agree on.
|
|
21
|
-
//
|
|
22
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps (the
|
|
23
|
-
// repo's node-≥23.6 convention).
|
|
24
|
-
|
|
25
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
26
|
-
import { dirname, join } from 'node:path'
|
|
27
|
-
import { pathToFileURL } from 'node:url'
|
|
28
|
-
import { parseSourcesToml, type SourceConfig } from '../../verify-scenarios/scripts/verify-scenarios.mts'
|
|
29
|
-
|
|
30
|
-
export type { SourceConfig }
|
|
31
|
-
|
|
32
|
-
const CONFIG_REL_PATH = '.agents/sdd/scenario-bridge.toml'
|
|
33
|
-
|
|
34
|
-
export function configPath(projectPath: string): string {
|
|
35
|
-
return join(projectPath, CONFIG_REL_PATH)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── list ──
|
|
39
|
-
|
|
40
|
-
// Every configured source in file order; a missing config lists nothing without error.
|
|
41
|
-
export function listSources(projectPath: string): SourceConfig[] {
|
|
42
|
-
const file = configPath(projectPath)
|
|
43
|
-
if (!existsSync(file)) return []
|
|
44
|
-
try {
|
|
45
|
-
return parseSourcesToml(readFileSync(file, 'utf8'))
|
|
46
|
-
} catch {
|
|
47
|
-
return []
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── render one [[source]] block ──
|
|
52
|
-
|
|
53
|
-
function renderBlock(source: { adapter: string; command?: string; reportPath: string }): string {
|
|
54
|
-
const lines = [
|
|
55
|
-
'[[source]]',
|
|
56
|
-
`adapter = "${source.adapter}"`,
|
|
57
|
-
...(source.command !== undefined ? [`command = "${source.command}"`] : []),
|
|
58
|
-
`reportPath = "${source.reportPath}"`,
|
|
59
|
-
]
|
|
60
|
-
return lines.join('\n')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── scaffold / add ──
|
|
64
|
-
|
|
65
|
-
export interface NewSourceInput {
|
|
66
|
-
adapter?: string
|
|
67
|
-
command?: string
|
|
68
|
-
reportPath?: string
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export type MutateResult = { ok: true; message: string; sources: SourceConfig[] } | { ok: false; reason: string }
|
|
72
|
-
|
|
73
|
-
function validateInput(
|
|
74
|
-
input: NewSourceInput,
|
|
75
|
-
): { ok: true; adapter: string; command?: string; reportPath: string } | { ok: false; reason: string } {
|
|
76
|
-
if (!input.adapter || !input.reportPath) {
|
|
77
|
-
return { ok: false, reason: 'a source needs both adapter and reportPath' }
|
|
78
|
-
}
|
|
79
|
-
return { ok: true, adapter: input.adapter, command: input.command, reportPath: input.reportPath }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Creates <project-path>/.agents/sdd/scenario-bridge.toml with one [[source]] block. Refuses when the
|
|
83
|
-
// file already exists (use `add` instead) or when adapter/reportPath is missing — no source block is
|
|
84
|
-
// written in the refused case.
|
|
85
|
-
export function scaffoldSource(projectPath: string, input: NewSourceInput): MutateResult {
|
|
86
|
-
const valid = validateInput(input)
|
|
87
|
-
if (!valid.ok) return valid
|
|
88
|
-
const file = configPath(projectPath)
|
|
89
|
-
if (existsSync(file)) {
|
|
90
|
-
return { ok: false, reason: 'scenario-bridge.toml already exists; use add to append a source' }
|
|
91
|
-
}
|
|
92
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
93
|
-
writeFileSync(file, `${renderBlock(valid)}\n`)
|
|
94
|
-
const sources = listSources(projectPath)
|
|
95
|
-
return { ok: true, message: `scaffolded ${file}`, sources }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Appends a [[source]] block to an existing config, leaving the existing sources untouched. Refuses
|
|
99
|
-
// when the config does not exist yet (names scaffold as the entry point) or when adapter/reportPath
|
|
100
|
-
// is missing.
|
|
101
|
-
export function addSource(projectPath: string, input: NewSourceInput): MutateResult {
|
|
102
|
-
const valid = validateInput(input)
|
|
103
|
-
if (!valid.ok) return valid
|
|
104
|
-
const file = configPath(projectPath)
|
|
105
|
-
if (!existsSync(file)) {
|
|
106
|
-
return { ok: false, reason: 'scenario-bridge.toml does not exist yet; use scaffold to create it first' }
|
|
107
|
-
}
|
|
108
|
-
const existing = readFileSync(file, 'utf8')
|
|
109
|
-
const sep = existing.endsWith('\n') ? '' : '\n'
|
|
110
|
-
writeFileSync(file, `${existing}${sep}\n${renderBlock(valid)}\n`)
|
|
111
|
-
const sources = listSources(projectPath)
|
|
112
|
-
return { ok: true, message: `added a source to ${file}`, sources }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── CLI ──
|
|
116
|
-
|
|
117
|
-
function flag(argv: string[], name: string): string | undefined {
|
|
118
|
-
const i = argv.indexOf(name)
|
|
119
|
-
return i === -1 ? undefined : argv[i + 1]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function inputFromArgv(argv: string[]): NewSourceInput {
|
|
123
|
-
return {
|
|
124
|
-
adapter: flag(argv, '--adapter'),
|
|
125
|
-
command: flag(argv, '--command'),
|
|
126
|
-
reportPath: flag(argv, '--report-path'),
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function main(argv: string[]): number {
|
|
131
|
-
const projectPath = flag(argv, '--project-path') ?? '.'
|
|
132
|
-
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
133
|
-
|
|
134
|
-
if (argv.includes('--list')) {
|
|
135
|
-
for (const s of listSources(projectPath)) {
|
|
136
|
-
w(`${s.adapter}${s.command ? ` (${s.command})` : ''} -> ${s.reportPath}`)
|
|
137
|
-
}
|
|
138
|
-
return 0
|
|
139
|
-
}
|
|
140
|
-
if (argv.includes('--scaffold')) {
|
|
141
|
-
const r = scaffoldSource(projectPath, inputFromArgv(argv))
|
|
142
|
-
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
143
|
-
return r.ok ? 0 : 1
|
|
144
|
-
}
|
|
145
|
-
if (argv.includes('--add')) {
|
|
146
|
-
const r = addSource(projectPath, inputFromArgv(argv))
|
|
147
|
-
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
148
|
-
return r.ok ? 0 : 1
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
w(
|
|
152
|
-
'usage: manage-scenario-bridge --project-path <dir> --list | --scaffold --adapter <a> [--command <c>] --report-path <p> | --add --adapter <a> [--command <c>] --report-path <p>',
|
|
153
|
-
)
|
|
154
|
-
return 0
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
158
|
-
process.exit(main(process.argv.slice(2)))
|
|
159
|
-
}
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// manage-spec-anchors — corpus/spec-anchors' concrete curation engine. Declares and curates the
|
|
3
|
-
// OPT-IN extra spec anchors that discover-specs scans on top of the three fixed conventions
|
|
4
|
-
// (ADR-0019). It reads/writes ONLY `.agents/sdd/spec-anchors.toml` — never a spec.md, status,
|
|
5
|
-
// approval, or freeze.
|
|
6
|
-
//
|
|
7
|
-
// Operations:
|
|
8
|
-
// --list list the three fixed anchors (each explained) + every custom anchor
|
|
9
|
-
// --add <pattern> validate + append a custom anchor (creates the config if absent)
|
|
10
|
-
// --remove <pattern> drop a custom anchor (no-op + unchanged when absent)
|
|
11
|
-
// --edit <old> <new> replace one custom anchor (rejects a malformed <new>)
|
|
12
|
-
// --induce <path> candidate patterns for a sample spec dir (literal + <project> capture)
|
|
13
|
-
// --preview <pattern> the project(s) a candidate pattern would discover, without persisting
|
|
14
|
-
//
|
|
15
|
-
// A pattern is a repo-relative directory pattern; `*` globs one segment, `**` globs zero or more
|
|
16
|
-
// segments (any depth), `<project>` globs AND captures a segment as the spec name. Fixed conventions
|
|
17
|
-
// are implicit and cannot be curated here.
|
|
18
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
|
|
19
|
-
|
|
20
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
|
|
21
|
-
import { dirname, join } from 'node:path'
|
|
22
|
-
import { pathToFileURL } from 'node:url'
|
|
23
|
-
|
|
24
|
-
const ANCHORS_CONFIG = '.agents/sdd/spec-anchors.toml'
|
|
25
|
-
const LIFECYCLE_STATUSES = new Set(['draft', 'approved', 'implemented', 'deprecated'])
|
|
26
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
|
|
27
|
-
|
|
28
|
-
export interface AnchorEntry {
|
|
29
|
-
pattern: string
|
|
30
|
-
kind: 'fixed' | 'custom'
|
|
31
|
-
explanation: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// The three fixed conventions, always scanned by discovery and never curated here.
|
|
35
|
-
export const FIXED_ANCHORS: AnchorEntry[] = [
|
|
36
|
-
{ pattern: '.agents/spec/', kind: 'fixed', explanation: 'the repo-root single-project spec' },
|
|
37
|
-
{
|
|
38
|
-
pattern: '.agents/specs/<project>/',
|
|
39
|
-
kind: 'fixed',
|
|
40
|
-
explanation: 'each repo-root multi-project spec (the folder names the project)',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
pattern: '<project-path>/.agents/spec/',
|
|
44
|
-
kind: 'fixed',
|
|
45
|
-
explanation: "a nested project's hoisted spec, at any depth",
|
|
46
|
-
},
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
// ── Config read/write (a minimal TOML: the `anchors = [ … ]` string array) ──
|
|
50
|
-
|
|
51
|
-
export function parseAnchorsToml(text: string): string[] {
|
|
52
|
-
const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text)
|
|
53
|
-
if (!m) return []
|
|
54
|
-
const out: string[] = []
|
|
55
|
-
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim())
|
|
56
|
-
return out.filter((s) => s !== '')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function readCustomAnchors(root: string): string[] {
|
|
60
|
-
const file = join(root, ANCHORS_CONFIG)
|
|
61
|
-
if (!existsSync(file)) return []
|
|
62
|
-
try {
|
|
63
|
-
return parseAnchorsToml(readFileSync(file, 'utf8'))
|
|
64
|
-
} catch {
|
|
65
|
-
return []
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function serializeAnchors(patterns: string[]): string {
|
|
70
|
-
if (patterns.length === 0) return 'anchors = []\n'
|
|
71
|
-
return `anchors = [\n${patterns.map((p) => ` "${p}",`).join('\n')}\n]\n`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writeCustomAnchors(root: string, patterns: string[]): void {
|
|
75
|
-
const file = join(root, ANCHORS_CONFIG)
|
|
76
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
77
|
-
writeFileSync(file, serializeAnchors(patterns))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Validation ──
|
|
81
|
-
|
|
82
|
-
// A pattern is well-formed iff it is repo-relative (no leading slash, no `..`) and every segment is
|
|
83
|
-
// a literal, `*`, `**`, or the `<project>` capture token (no other `<…>` token, no empty segment).
|
|
84
|
-
export function isValidPattern(pattern: string): boolean {
|
|
85
|
-
const p = pattern.trim().replace(/\\/g, '/')
|
|
86
|
-
if (p === '' || p.startsWith('/') || /(^|\/)\.\.(\/|$)/.test(p)) return false
|
|
87
|
-
const segs = p.replace(/^\/+|\/+$/g, '').split('/')
|
|
88
|
-
if (segs.some((s) => s === '')) return false
|
|
89
|
-
return segs.every((s) => s === '*' || s === '**' || s === '<project>' || !/[<>]/.test(s))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// True when a pattern names one of the three fixed conventions (which are implicit, not curated).
|
|
93
|
-
export function isFixedConvention(pattern: string): boolean {
|
|
94
|
-
const p = pattern
|
|
95
|
-
.trim()
|
|
96
|
-
.replace(/\\/g, '/')
|
|
97
|
-
.replace(/^\/+|\/+$/g, '')
|
|
98
|
-
return (
|
|
99
|
-
p === '.agents/spec' ||
|
|
100
|
-
p === '.agents/specs/<project>' ||
|
|
101
|
-
p === '.agents/specs/*' ||
|
|
102
|
-
p.endsWith('/.agents/spec') ||
|
|
103
|
-
p.endsWith('/.agents/specs/<project>')
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── list ──
|
|
108
|
-
|
|
109
|
-
export function listAnchors(root: string): AnchorEntry[] {
|
|
110
|
-
const custom = readCustomAnchors(root).map(
|
|
111
|
-
(pattern): AnchorEntry => ({ pattern, kind: 'custom', explanation: 'a declared extra anchor' }),
|
|
112
|
-
)
|
|
113
|
-
return [...FIXED_ANCHORS, ...custom]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── add / remove / edit ──
|
|
117
|
-
|
|
118
|
-
export type CrudResult =
|
|
119
|
-
| { ok: true; changed: boolean; anchors: string[]; message: string }
|
|
120
|
-
| { ok: false; reason: string }
|
|
121
|
-
|
|
122
|
-
export function addAnchor(root: string, pattern: string): CrudResult {
|
|
123
|
-
const p = pattern.trim()
|
|
124
|
-
if (isFixedConvention(p)) return { ok: false, reason: 'that is a fixed convention — always scanned, never curated' }
|
|
125
|
-
if (!isValidPattern(p)) return { ok: false, reason: 'malformed anchor pattern' }
|
|
126
|
-
const current = readCustomAnchors(root)
|
|
127
|
-
if (current.includes(p)) return { ok: true, changed: false, anchors: current, message: 'already present' }
|
|
128
|
-
const next = [...current, p]
|
|
129
|
-
writeCustomAnchors(root, next)
|
|
130
|
-
return { ok: true, changed: true, anchors: next, message: `added ${p}` }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function removeAnchor(root: string, pattern: string): CrudResult {
|
|
134
|
-
const p = pattern.trim()
|
|
135
|
-
const current = readCustomAnchors(root)
|
|
136
|
-
if (!current.includes(p)) return { ok: true, changed: false, anchors: current, message: 'nothing was removed' }
|
|
137
|
-
const next = current.filter((a) => a !== p)
|
|
138
|
-
writeCustomAnchors(root, next)
|
|
139
|
-
return { ok: true, changed: true, anchors: next, message: `removed ${p}` }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function editAnchor(root: string, oldPattern: string, newPattern: string): CrudResult {
|
|
143
|
-
const oldP = oldPattern.trim()
|
|
144
|
-
const newP = newPattern.trim()
|
|
145
|
-
if (isFixedConvention(oldP)) return { ok: false, reason: 'cannot edit a fixed convention' }
|
|
146
|
-
if (!isValidPattern(newP)) return { ok: false, reason: 'malformed replacement pattern' }
|
|
147
|
-
if (isFixedConvention(newP)) return { ok: false, reason: 'replacement is a fixed convention' }
|
|
148
|
-
const current = readCustomAnchors(root)
|
|
149
|
-
if (!current.includes(oldP)) return { ok: false, reason: `no such custom anchor: ${oldP}` }
|
|
150
|
-
const next = current.map((a) => (a === oldP ? newP : a))
|
|
151
|
-
writeCustomAnchors(root, next)
|
|
152
|
-
return { ok: true, changed: true, anchors: next, message: `edited ${oldP} -> ${newP}` }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── induce ──
|
|
156
|
-
|
|
157
|
-
export type InduceResult = { ok: true; candidates: string[] } | { ok: false; reason: string }
|
|
158
|
-
|
|
159
|
-
// From a sample path to a spec's directory, offer a literal-directory candidate and a <project>-token
|
|
160
|
-
// generalization (deepest segment replaced with the capture). Rejects a path that is not a directory
|
|
161
|
-
// under the repo root.
|
|
162
|
-
export function inducePatterns(root: string, samplePath: string): InduceResult {
|
|
163
|
-
const rel = samplePath
|
|
164
|
-
.trim()
|
|
165
|
-
.replace(/\\/g, '/')
|
|
166
|
-
.replace(/^\/+|\/+$/g, '')
|
|
167
|
-
if (rel === '' || /(^|\/)\.\.(\/|$)/.test(rel)) return { ok: false, reason: 'path is not repo-relative' }
|
|
168
|
-
let ok = false
|
|
169
|
-
try {
|
|
170
|
-
ok = statSync(join(root, rel)).isDirectory()
|
|
171
|
-
} catch {
|
|
172
|
-
ok = false
|
|
173
|
-
}
|
|
174
|
-
if (!ok) return { ok: false, reason: `sample path does not resolve to a directory under the repo: ${rel}` }
|
|
175
|
-
const segs = rel.split('/')
|
|
176
|
-
const generalized = [...segs.slice(0, -1), '<project>'].join('/')
|
|
177
|
-
const candidates = generalized === rel ? [rel] : [rel, generalized]
|
|
178
|
-
return { ok: true, candidates }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── preview ──
|
|
182
|
-
|
|
183
|
-
interface PreviewMatch {
|
|
184
|
-
path: string
|
|
185
|
-
name: string
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export type PreviewResult = { ok: true; matches: PreviewMatch[] } | { ok: false; reason: string }
|
|
189
|
-
|
|
190
|
-
// Every dir reachable from `startDir` by descending zero or more levels (startDir itself included),
|
|
191
|
-
// skipping SKIP_DIRS. Backs the `**` segment (any-depth glob) below.
|
|
192
|
-
function collectDescendants(root: string, startDir: string): string[] {
|
|
193
|
-
const out = [startDir]
|
|
194
|
-
let entries: import('node:fs').Dirent[]
|
|
195
|
-
try {
|
|
196
|
-
entries = readdirSync(join(root, startDir), { withFileTypes: true })
|
|
197
|
-
} catch {
|
|
198
|
-
return out
|
|
199
|
-
}
|
|
200
|
-
for (const e of entries) {
|
|
201
|
-
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue
|
|
202
|
-
out.push(...collectDescendants(root, startDir ? `${startDir}/${e.name}` : e.name))
|
|
203
|
-
}
|
|
204
|
-
return out
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Expand a pattern against the filesystem (mirrors discover-specs' expandAnchor), then keep only the
|
|
208
|
-
// dirs holding a spec.md whose frontmatter status is in the lifecycle enum — the specs discovery
|
|
209
|
-
// would actually surface. Does NOT read or persist anything to the config.
|
|
210
|
-
export function previewPattern(root: string, pattern: string): PreviewResult {
|
|
211
|
-
const p = pattern.trim()
|
|
212
|
-
if (!isValidPattern(p)) return { ok: false, reason: 'malformed candidate pattern' }
|
|
213
|
-
const segs = p
|
|
214
|
-
.replace(/\\/g, '/')
|
|
215
|
-
.replace(/^\/+|\/+$/g, '')
|
|
216
|
-
.split('/')
|
|
217
|
-
.filter(Boolean)
|
|
218
|
-
let frontier: { dir: string; capturedName?: string }[] = [{ dir: '' }]
|
|
219
|
-
for (const seg of segs) {
|
|
220
|
-
const next: { dir: string; capturedName?: string }[] = []
|
|
221
|
-
if (seg === '**') {
|
|
222
|
-
for (const node of frontier) {
|
|
223
|
-
for (const dir of collectDescendants(root, node.dir)) next.push({ dir, capturedName: node.capturedName })
|
|
224
|
-
}
|
|
225
|
-
frontier = next
|
|
226
|
-
continue
|
|
227
|
-
}
|
|
228
|
-
const isGlob = seg === '*' || seg === '<project>'
|
|
229
|
-
for (const node of frontier) {
|
|
230
|
-
if (isGlob) {
|
|
231
|
-
let entries: import('node:fs').Dirent[]
|
|
232
|
-
try {
|
|
233
|
-
entries = readdirSync(join(root, node.dir), { withFileTypes: true })
|
|
234
|
-
} catch {
|
|
235
|
-
continue
|
|
236
|
-
}
|
|
237
|
-
for (const e of entries) {
|
|
238
|
-
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue
|
|
239
|
-
next.push({
|
|
240
|
-
dir: node.dir ? `${node.dir}/${e.name}` : e.name,
|
|
241
|
-
capturedName: seg === '<project>' ? e.name : node.capturedName,
|
|
242
|
-
})
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
next.push({ dir: node.dir ? `${node.dir}/${seg}` : seg, capturedName: node.capturedName })
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
frontier = next
|
|
249
|
-
}
|
|
250
|
-
const matches: PreviewMatch[] = []
|
|
251
|
-
for (const node of frontier) {
|
|
252
|
-
const rel = node.dir ? `${node.dir}/spec.md` : 'spec.md'
|
|
253
|
-
let status: string | undefined
|
|
254
|
-
try {
|
|
255
|
-
status = /(^|\n)status:\s*["']?([a-z]+)/.exec(readFileSync(join(root, rel), 'utf8'))?.[2]
|
|
256
|
-
} catch {
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
if (!status || !LIFECYCLE_STATUSES.has(status)) continue
|
|
260
|
-
matches.push({ path: node.dir, name: node.capturedName ?? node.dir.split('/').pop() ?? node.dir })
|
|
261
|
-
}
|
|
262
|
-
matches.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
263
|
-
return { ok: true, matches }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── CLI ──
|
|
267
|
-
|
|
268
|
-
function flag(argv: string[], name: string): string | undefined {
|
|
269
|
-
const i = argv.indexOf(name)
|
|
270
|
-
return i === -1 ? undefined : argv[i + 1]
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function main(argv: string[]): number {
|
|
274
|
-
const root = flag(argv, '--root') ?? '.'
|
|
275
|
-
const w = (s: string) => process.stdout.write(`${s}\n`)
|
|
276
|
-
|
|
277
|
-
if (argv.includes('--list')) {
|
|
278
|
-
w('Spec anchors discovery scans (fixed conventions are always scanned):')
|
|
279
|
-
for (const a of listAnchors(root)) w(` [${a.kind}] ${a.pattern} — ${a.explanation}`)
|
|
280
|
-
return 0
|
|
281
|
-
}
|
|
282
|
-
if (argv.includes('--add')) {
|
|
283
|
-
const r = addAnchor(root, flag(argv, '--add') ?? '')
|
|
284
|
-
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
285
|
-
return r.ok ? 0 : 1
|
|
286
|
-
}
|
|
287
|
-
if (argv.includes('--remove')) {
|
|
288
|
-
const r = removeAnchor(root, flag(argv, '--remove') ?? '')
|
|
289
|
-
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
290
|
-
return r.ok ? 0 : 1
|
|
291
|
-
}
|
|
292
|
-
if (argv.includes('--edit')) {
|
|
293
|
-
const i = argv.indexOf('--edit')
|
|
294
|
-
const r = editAnchor(root, argv[i + 1] ?? '', argv[i + 2] ?? '')
|
|
295
|
-
w(r.ok ? r.message : `refused: ${r.reason}`)
|
|
296
|
-
return r.ok ? 0 : 1
|
|
297
|
-
}
|
|
298
|
-
if (argv.includes('--induce')) {
|
|
299
|
-
const r = inducePatterns(root, flag(argv, '--induce') ?? '')
|
|
300
|
-
if (!r.ok) {
|
|
301
|
-
w(`unusable: ${r.reason}`)
|
|
302
|
-
return 1
|
|
303
|
-
}
|
|
304
|
-
w('Candidate anchor patterns:')
|
|
305
|
-
for (const c of r.candidates) w(` ${c}`)
|
|
306
|
-
return 0
|
|
307
|
-
}
|
|
308
|
-
if (argv.includes('--preview')) {
|
|
309
|
-
const r = previewPattern(root, flag(argv, '--preview') ?? '')
|
|
310
|
-
if (!r.ok) {
|
|
311
|
-
w(`invalid: ${r.reason}`)
|
|
312
|
-
return 1
|
|
313
|
-
}
|
|
314
|
-
if (r.matches.length === 0) {
|
|
315
|
-
w('This pattern matches no project.')
|
|
316
|
-
return 0
|
|
317
|
-
}
|
|
318
|
-
w('This pattern would discover:')
|
|
319
|
-
for (const m of r.matches) w(` ${m.path} (${m.name})`)
|
|
320
|
-
return 0
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
w(
|
|
324
|
-
'usage: manage-spec-anchors --list | --add <p> | --remove <p> | --edit <old> <new> | --induce <path> | --preview <p>',
|
|
325
|
-
)
|
|
326
|
-
return 0
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
330
|
-
process.exit(main(process.argv.slice(2)))
|
|
331
|
-
}
|