@warnyin/sdlc 0.5.0 → 0.5.2
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/CHANGELOG.md +25 -0
- package/LICENSE +21 -21
- package/bin/cli.mjs +12 -1
- package/lib/caps.mjs +45 -45
- package/lib/config.mjs +41 -41
- package/lib/delta.mjs +227 -160
- package/lib/frontmatter.mjs +59 -59
- package/lib/glob.mjs +29 -29
- package/lib/manifest.mjs +99 -99
- package/lib/settings-merge.mjs +63 -63
- package/lib/usage.mjs +49 -46
- package/lib/validate.mjs +196 -186
- package/package.json +42 -42
- package/payload/adapters/agents-md.md +8 -8
- package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
- package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
- package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
- package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
- package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
- package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
- package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
- package/payload/adapters/claude/agents/sdlc-security.md +12 -12
- package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
- package/payload/adapters/claude/commands/sdlc/init.md +4 -4
- package/payload/adapters/claude/commands/sdlc/next.md +4 -4
- package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
- package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
- package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
- package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +36 -33
- package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +26 -26
- package/payload/adapters/cline.md +8 -8
- package/payload/adapters/copilot.md +8 -8
- package/payload/adapters/cursor.mdc +7 -7
- package/payload/adapters/gemini.md +8 -8
- package/payload/adapters/windsurf.md +4 -4
- package/payload/hooks/_shared.mjs +154 -154
- package/payload/hooks/guard-writes.mjs +83 -83
- package/payload/hooks/inject-context.mjs +55 -55
- package/payload/hooks/journal.mjs +58 -58
- package/payload/hooks/session-summary.mjs +50 -50
- package/payload/hooks/validate-artifact.mjs +80 -80
- package/payload/playbook/context.md +26 -26
- package/payload/playbook/converge.md +19 -19
- package/payload/playbook/init.md +22 -22
- package/payload/playbook/observe.md +20 -20
- package/payload/playbook/principles.md +28 -28
- package/payload/playbook/routing.md +19 -19
- package/payload/playbook/rules-card.md +16 -16
- package/payload/playbook/ship.md +2 -0
- package/payload/playbook/steer.md +21 -21
- package/payload/templates/change-deep.md +29 -29
- package/payload/templates/change-standard.md +28 -28
- package/payload/templates/change-vibe.md +19 -19
- package/payload/templates/config.yaml +8 -8
- package/payload/templates/constitution.md +14 -14
- package/payload/templates/contract-evals.md +9 -9
- package/payload/templates/contract-tests.md +9 -9
- package/payload/templates/harness.md +33 -33
- package/payload/templates/spec.md +14 -14
- package/payload/templates/steering.md +9 -9
- package/scripts/validate.mjs +47 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2 (2026-08-25)
|
|
4
|
+
|
|
5
|
+
- **Fix (delta)**: a `### MODIFIED Requirement:` body replaces the requirement wholesale,
|
|
6
|
+
so one that carried over only some of the spec's scenarios dropped the rest in silence —
|
|
7
|
+
no error, no warning, `spec merged` printed either way. Both shapes are now reported:
|
|
8
|
+
the scenario name gone from the replacement body, and the name surviving while WHEN/THEN
|
|
9
|
+
clauses it promised have no counterpart (the one a name-level comparison cannot see).
|
|
10
|
+
`archive` prints the report before it writes a byte and counts it in the summary;
|
|
11
|
+
`validate` reports the same at warn level, so the loss is visible while the change folder
|
|
12
|
+
is still readable rather than after ship archived it. A warning, never an error — removing
|
|
13
|
+
a scenario is sometimes the point of the change, and only the silence was ever the bug.
|
|
14
|
+
A reworded clause reports the same as a deleted one: nothing mechanical can tell "said
|
|
15
|
+
better" from "promises less". Cosmetic churn — indentation, bullet marker, clause order,
|
|
16
|
+
heading case, whitespace — is normalized away and never warns. (#1)
|
|
17
|
+
|
|
18
|
+
## 0.5.1 (2026-08-25)
|
|
19
|
+
|
|
20
|
+
- **Fix (cost)**: `costUsd()` never charged cache-write tokens, the highest-rate of
|
|
21
|
+
the four classes the usage parser collects. Every cost `/sdlc:observe` and the
|
|
22
|
+
session summary have printed was therefore low. A model priced without a
|
|
23
|
+
`cacheWrite` rate now charges nothing for that class rather than inferring one from
|
|
24
|
+
`input` — the module's rule is never to guess a price, and a guess reports as
|
|
25
|
+
confidently as a known rate. Existing journalled costs are left alone: backfilling
|
|
26
|
+
would rewrite history from a rate that was not in force at the time.
|
|
27
|
+
|
|
3
28
|
## 0.5.0 (2026-08-25)
|
|
4
29
|
|
|
5
30
|
- **`--auto` on every pipeline stage.** `/sdlc:auto` already ran the whole pipeline,
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 warnyin
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 warnyin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/cli.mjs
CHANGED
|
@@ -479,14 +479,22 @@ export function cmdArchive(projectRoot, changeId, { strict = true } = {}) {
|
|
|
479
479
|
|
|
480
480
|
// Phase 1: compute every merge before writing anything (all-or-nothing).
|
|
481
481
|
const merged = [];
|
|
482
|
+
const driftWarnings = [];
|
|
482
483
|
for (const d of deltas) {
|
|
483
484
|
const specPath = path.join(sdlcRoot, 'specs', d.capability, 'spec.md');
|
|
484
485
|
const specText = fs.existsSync(specPath) ? fs.readFileSync(specPath, 'utf8') : null;
|
|
485
486
|
const result = mergeDelta(specText, d.ops, d.capability);
|
|
486
487
|
if (!result.ok) throw new Error(`spec merge failed for "${d.capability}": ${result.errors.join('; ')}`);
|
|
488
|
+
driftWarnings.push(...result.warnings);
|
|
487
489
|
merged.push({ specPath, content: result.content, capability: d.capability });
|
|
488
490
|
}
|
|
489
491
|
|
|
492
|
+
// A MODIFIED body replaces the requirement wholesale, so it can carry away a
|
|
493
|
+
// scenario the spec still promised. That is allowed — but it is said out loud
|
|
494
|
+
// here, while the change folder is still readable, not discovered in a diff
|
|
495
|
+
// after the folder moved under changes/archive/.
|
|
496
|
+
for (const w of driftWarnings) console.error(`⚠ ${w}`);
|
|
497
|
+
|
|
490
498
|
// Phase 2: write specs, promote evals, stamp status, move to archive.
|
|
491
499
|
for (const m of merged) writeFileNormalized(m.specPath, m.content);
|
|
492
500
|
|
|
@@ -508,8 +516,11 @@ export function cmdArchive(projectRoot, changeId, { strict = true } = {}) {
|
|
|
508
516
|
|
|
509
517
|
console.log(`shipped: ${changeId}`);
|
|
510
518
|
for (const m of merged) console.log(` spec merged: specs/${m.capability}/spec.md`);
|
|
519
|
+
if (driftWarnings.length) {
|
|
520
|
+
console.log(` ⚠ ${driftWarnings.length} scenario warning(s) above — re-read the spec diff before pushing`);
|
|
521
|
+
}
|
|
511
522
|
console.log(` archived: changes/archive/${date}-${changeId}/`);
|
|
512
|
-
return { archived: `${date}-${changeId}`, specs: merged.map((m) => m.capability) };
|
|
523
|
+
return { archived: `${date}-${changeId}`, specs: merged.map((m) => m.capability), warnings: driftWarnings };
|
|
513
524
|
}
|
|
514
525
|
|
|
515
526
|
// ---------- shared ----------
|
package/lib/caps.mjs
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
// Canonical line-cap table — the single source of truth for artifact
|
|
2
|
-
// residency budgets. Templates quote these numbers in HTML comments and
|
|
3
|
-
// tests/caps-sync.test.mjs asserts both stay identical.
|
|
4
|
-
//
|
|
5
|
-
// Rationale (Day-1 economics): every resident line is paid for in every
|
|
6
|
-
// turn. Caps make "learning = distilling" structural, not disciplinary.
|
|
7
|
-
|
|
8
|
-
import { parseFrontmatter } from './frontmatter.mjs';
|
|
9
|
-
|
|
10
|
-
export const CAPS = Object.freeze({
|
|
11
|
-
constitution: 30, // sdlc/context/constitution.md — always loaded
|
|
12
|
-
steeringFile: 40, // each sdlc/context/steering/*.md
|
|
13
|
-
alwaysBudget: 60, // constitution + all `inclusion: always` steering, combined
|
|
14
|
-
harness: 60, // sdlc/harness.md
|
|
15
|
-
spec: 150, // sdlc/specs/<capability>/spec.md (soft — split capability beyond)
|
|
16
|
-
change: Object.freeze({ vibe: 40, standard: 100, deep: 150 }),
|
|
17
|
-
contractTests: 60, // changes/<id>/contract/tests.md
|
|
18
|
-
contractEvals: 40, // changes/<id>/contract/evals.md
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export const TIERS = Object.freeze(['vibe', 'standard', 'deep']);
|
|
22
|
-
|
|
23
|
-
export const STATUSES = Object.freeze([
|
|
24
|
-
'new', 'contracted', 'building', 'verified', 'shipped',
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
// Effective lines = body lines after frontmatter, excluding blanks and
|
|
28
|
-
// single-line HTML comments. Caps meter prose the model must carry, not
|
|
29
|
-
// machine metadata or annotation comments.
|
|
30
|
-
export function countEffectiveLines(text) {
|
|
31
|
-
const { body } = parseFrontmatter(text ?? '');
|
|
32
|
-
return body
|
|
33
|
-
.split(/\r?\n/)
|
|
34
|
-
.filter((line) => {
|
|
35
|
-
const t = line.trim();
|
|
36
|
-
if (t === '') return false;
|
|
37
|
-
if (t.startsWith('<!--') && t.endsWith('-->')) return false;
|
|
38
|
-
return true;
|
|
39
|
-
})
|
|
40
|
-
.length;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function capForChange(tier) {
|
|
44
|
-
return CAPS.change[tier] ?? CAPS.change.standard;
|
|
45
|
-
}
|
|
1
|
+
// Canonical line-cap table — the single source of truth for artifact
|
|
2
|
+
// residency budgets. Templates quote these numbers in HTML comments and
|
|
3
|
+
// tests/caps-sync.test.mjs asserts both stay identical.
|
|
4
|
+
//
|
|
5
|
+
// Rationale (Day-1 economics): every resident line is paid for in every
|
|
6
|
+
// turn. Caps make "learning = distilling" structural, not disciplinary.
|
|
7
|
+
|
|
8
|
+
import { parseFrontmatter } from './frontmatter.mjs';
|
|
9
|
+
|
|
10
|
+
export const CAPS = Object.freeze({
|
|
11
|
+
constitution: 30, // sdlc/context/constitution.md — always loaded
|
|
12
|
+
steeringFile: 40, // each sdlc/context/steering/*.md
|
|
13
|
+
alwaysBudget: 60, // constitution + all `inclusion: always` steering, combined
|
|
14
|
+
harness: 60, // sdlc/harness.md
|
|
15
|
+
spec: 150, // sdlc/specs/<capability>/spec.md (soft — split capability beyond)
|
|
16
|
+
change: Object.freeze({ vibe: 40, standard: 100, deep: 150 }),
|
|
17
|
+
contractTests: 60, // changes/<id>/contract/tests.md
|
|
18
|
+
contractEvals: 40, // changes/<id>/contract/evals.md
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const TIERS = Object.freeze(['vibe', 'standard', 'deep']);
|
|
22
|
+
|
|
23
|
+
export const STATUSES = Object.freeze([
|
|
24
|
+
'new', 'contracted', 'building', 'verified', 'shipped',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Effective lines = body lines after frontmatter, excluding blanks and
|
|
28
|
+
// single-line HTML comments. Caps meter prose the model must carry, not
|
|
29
|
+
// machine metadata or annotation comments.
|
|
30
|
+
export function countEffectiveLines(text) {
|
|
31
|
+
const { body } = parseFrontmatter(text ?? '');
|
|
32
|
+
return body
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.filter((line) => {
|
|
35
|
+
const t = line.trim();
|
|
36
|
+
if (t === '') return false;
|
|
37
|
+
if (t.startsWith('<!--') && t.endsWith('-->')) return false;
|
|
38
|
+
return true;
|
|
39
|
+
})
|
|
40
|
+
.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function capForChange(tier) {
|
|
44
|
+
return CAPS.change[tier] ?? CAPS.change.standard;
|
|
45
|
+
}
|
package/lib/config.mjs
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
// Minimal reader for sdlc/config.yaml — supports exactly what the template
|
|
2
|
-
// documents: scalar keys, inline arrays, and a `prices:` block of inline
|
|
3
|
-
// objects. Anything else is ignored (never crash a hook on config).
|
|
4
|
-
|
|
5
|
-
export function parseConfig(text) {
|
|
6
|
-
const config = { language: 'en', tools: [], prices: null };
|
|
7
|
-
const lines = (text ?? '').split(/\r?\n/);
|
|
8
|
-
let inPrices = false;
|
|
9
|
-
for (const raw of lines) {
|
|
10
|
-
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
|
11
|
-
const isIndented = /^\s/.test(raw);
|
|
12
|
-
if (!isIndented) inPrices = false;
|
|
13
|
-
|
|
14
|
-
if (inPrices) {
|
|
15
|
-
const m = raw.match(/^\s+([^:#]+):\s*\{(.*)\}\s*$/);
|
|
16
|
-
if (!m) continue;
|
|
17
|
-
const model = m[1].trim();
|
|
18
|
-
const obj = {};
|
|
19
|
-
for (const part of m[2].split(',')) {
|
|
20
|
-
const kv = part.split(':');
|
|
21
|
-
if (kv.length !== 2) continue;
|
|
22
|
-
const num = Number(kv[1].trim());
|
|
23
|
-
if (!Number.isNaN(num)) obj[kv[0].trim()] = num;
|
|
24
|
-
}
|
|
25
|
-
config.prices = { ...(config.prices ?? {}), [model]: obj };
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
30
|
-
if (!kv) continue;
|
|
31
|
-
const [, key, valueRaw] = kv;
|
|
32
|
-
const value = valueRaw.replace(/\s+#.*$/, '').trim();
|
|
33
|
-
if (key === 'prices' && value === '') { inPrices = true; continue; }
|
|
34
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
35
|
-
config[key] = value.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
|
|
36
|
-
} else if (value !== '') {
|
|
37
|
-
config[key] = value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return config;
|
|
41
|
-
}
|
|
1
|
+
// Minimal reader for sdlc/config.yaml — supports exactly what the template
|
|
2
|
+
// documents: scalar keys, inline arrays, and a `prices:` block of inline
|
|
3
|
+
// objects. Anything else is ignored (never crash a hook on config).
|
|
4
|
+
|
|
5
|
+
export function parseConfig(text) {
|
|
6
|
+
const config = { language: 'en', tools: [], prices: null };
|
|
7
|
+
const lines = (text ?? '').split(/\r?\n/);
|
|
8
|
+
let inPrices = false;
|
|
9
|
+
for (const raw of lines) {
|
|
10
|
+
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
|
11
|
+
const isIndented = /^\s/.test(raw);
|
|
12
|
+
if (!isIndented) inPrices = false;
|
|
13
|
+
|
|
14
|
+
if (inPrices) {
|
|
15
|
+
const m = raw.match(/^\s+([^:#]+):\s*\{(.*)\}\s*$/);
|
|
16
|
+
if (!m) continue;
|
|
17
|
+
const model = m[1].trim();
|
|
18
|
+
const obj = {};
|
|
19
|
+
for (const part of m[2].split(',')) {
|
|
20
|
+
const kv = part.split(':');
|
|
21
|
+
if (kv.length !== 2) continue;
|
|
22
|
+
const num = Number(kv[1].trim());
|
|
23
|
+
if (!Number.isNaN(num)) obj[kv[0].trim()] = num;
|
|
24
|
+
}
|
|
25
|
+
config.prices = { ...(config.prices ?? {}), [model]: obj };
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const kv = raw.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
30
|
+
if (!kv) continue;
|
|
31
|
+
const [, key, valueRaw] = kv;
|
|
32
|
+
const value = valueRaw.replace(/\s+#.*$/, '').trim();
|
|
33
|
+
if (key === 'prices' && value === '') { inPrices = true; continue; }
|
|
34
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
35
|
+
config[key] = value.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
|
|
36
|
+
} else if (value !== '') {
|
|
37
|
+
config[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|