yadflow 3.16.3 → 3.17.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/CHANGELOG.md +21 -0
- package/README.md +4 -0
- package/bin/yad.mjs +24 -2
- package/cli/commit.mjs +1 -1
- package/cli/doctor.mjs +55 -1
- package/cli/gate.mjs +5 -6
- package/cli/hook.mjs +224 -0
- package/cli/manifest.mjs +40 -0
- package/cli/next.mjs +1 -1
- package/cli/plan.mjs +160 -5
- package/cli/reconcile.mjs +2 -2
- package/cli/setup.mjs +10 -1
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +1 -1
- package/skills/yad-analysis/SKILL.md +1 -1
- package/skills/yad-checks/SKILL.md +42 -0
- package/skills/yad-checks/references/check-gates.md +109 -3
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +69 -0
- package/skills/yad-connect-learning/references/learning-registry.md +1 -1
- package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
- package/skills/yad-epic/SKILL.md +1 -1
- package/skills/yad-epic/references/state-schema.md +6 -6
- package/skills/yad-hub-bridge/SKILL.md +3 -1
- package/skills/yad-implement/SKILL.md +3 -3
- package/skills/yad-implement/references/implement-conventions.md +7 -7
- package/skills/yad-review-gate/SKILL.md +3 -1
- package/skills/yad-spec/SKILL.md +1 -1
- package/skills/yad-spec/references/spec-handoff.md +3 -3
- package/skills/yad-status/SKILL.md +1 -1
- package/skills/yad-stories/SKILL.md +1 -1
- package/skills/yad-test-cases/SKILL.md +1 -1
- package/skills/yad-test-cases/references/test-cases-schema.md +1 -1
- package/skills/yad-ui/SKILL.md +1 -1
package/cli/plan.mjs
CHANGED
|
@@ -5,10 +5,11 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { err } from './errors.mjs';
|
|
7
7
|
import {
|
|
8
|
-
asset, exists, copyDir, copyFile, dirMatches, sameContent, readJSON, readJSONStrict, writeJSON, fileSha,
|
|
8
|
+
asset, exists, copyDir, copyFile, dirMatches, sameContent, readJSON, readJSONStrict, writeJSON, fileSha, warn,
|
|
9
9
|
} from './lib.mjs';
|
|
10
10
|
import {
|
|
11
|
-
VERSION, SKILLS, IDE_TARGETS, IDE_OPENCODE_DIR, MODULE_FILES, wiringFor, HUB_WIRING, PROJECT_FILES,
|
|
11
|
+
VERSION, SKILLS, IDE_TARGETS, IDE_OPENCODE_DIR, MODULE_FILES, wiringFor, HUB_WIRING, PROJECT_FILES, isBridgeHub,
|
|
12
|
+
HOOK_WIRING, HOOK_SETTINGS, HOOK_TOOL_MATCHER, HOOK_COMMAND, HOOK_COMMAND_LEGACY,
|
|
12
13
|
LEGACY_SKILLS, REMOVED_SKILLS, LEGACY_MARKER, LEGACY_REPO_FILES, LEGACY_HUB_FILES, MANAGED_LEDGER, BACKUP_SUFFIX,
|
|
13
14
|
} from './manifest.mjs';
|
|
14
15
|
|
|
@@ -441,7 +442,7 @@ export function legacyRepoActions(root, repo) {
|
|
|
441
442
|
|
|
442
443
|
export function legacyHubActions(root) {
|
|
443
444
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
444
|
-
if (!
|
|
445
|
+
if (!isBridgeHub(hub)) return [];
|
|
445
446
|
const wiring = [...HUB_WIRING.common, ...(HUB_WIRING[hub.platform] || [])];
|
|
446
447
|
return legacyFileActions('hub', root, LEGACY_HUB_FILES[hub.platform], wiring);
|
|
447
448
|
}
|
|
@@ -460,14 +461,168 @@ export function repoActions(root, repo) {
|
|
|
460
461
|
export function hubActions(root) {
|
|
461
462
|
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
462
463
|
// `bridge_enabled` is the canonical flag (the documented hub-config schema); older setup versions
|
|
463
|
-
// wrote `bridge` —
|
|
464
|
-
|
|
464
|
+
// wrote `bridge` — `isBridgeHub` accepts an explicit true in either spelling, and is the one
|
|
465
|
+
// predicate the CLI, the wiring, and the ledger hook all read (#186). Wire nothing otherwise.
|
|
466
|
+
if (!isBridgeHub(hub)) return [];
|
|
465
467
|
const ledger = readManagedLedger(root);
|
|
466
468
|
return [...HUB_WIRING.common, ...(HUB_WIRING[hub.platform] || [])].map((w) =>
|
|
467
469
|
wiredFileAction('hub', w.dest, asset(w.src), path.join(root, w.dest), { root, exec: !!w.exec, ledger }),
|
|
468
470
|
);
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
// ---- harness hooks (#171) --------------------------------------------------------------------
|
|
474
|
+
// The desired hook entry, in the shape a harness reads it.
|
|
475
|
+
export const hookEntry = () => ({
|
|
476
|
+
matcher: HOOK_TOOL_MATCHER,
|
|
477
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Ours is a hook command EXACTLY equal to one we have written — the current spelling or a
|
|
481
|
+
// documented past one. Never "the entry at index N", never "the entry with our matcher", and
|
|
482
|
+
// deliberately never a substring test: `includes('hooks/ledger-guard.sh')` would also claim a team's
|
|
483
|
+
// own wrapper at `.claude/hooks/ledger-guard.sh` and silently rewrite it to ours, on the `outdated`
|
|
484
|
+
// path that takes no backup. Matching exactly means the worst case is a second entry (the guard runs
|
|
485
|
+
// twice — harmless) instead of someone else's hook disappearing.
|
|
486
|
+
const OWNED_COMMANDS = new Set([HOOK_COMMAND, ...HOOK_COMMAND_LEGACY]);
|
|
487
|
+
const OURS = (h) => typeof h?.command === 'string' && OWNED_COMMANDS.has(h.command);
|
|
488
|
+
|
|
489
|
+
// Additive merge of our PreToolUse entry into a parsed settings object. Returns
|
|
490
|
+
// `{ settings, changed }`; `settings` is a new object, so a caller can compare without mutating.
|
|
491
|
+
// A matcher the team NARROWED is left alone (only the command is normalised) — the same respect for
|
|
492
|
+
// a local edit that `modified` gives a managed file. Widening it back would silently undo their choice.
|
|
493
|
+
export function mergeHookSettings(input) {
|
|
494
|
+
const settings = { ...(input && typeof input === 'object' && !Array.isArray(input) ? input : {}) };
|
|
495
|
+
const hooks = { ...(settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks) ? settings.hooks : {}) };
|
|
496
|
+
const pre = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.map((e) => ({ ...e })) : [];
|
|
497
|
+
let changed = false;
|
|
498
|
+
let found = false;
|
|
499
|
+
for (const entry of pre) {
|
|
500
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
501
|
+
entry.hooks = entry.hooks.map((h) => {
|
|
502
|
+
if (!OURS(h)) return h;
|
|
503
|
+
found = true;
|
|
504
|
+
if (h.command === HOOK_COMMAND && h.type === 'command') return h;
|
|
505
|
+
changed = true;
|
|
506
|
+
return { ...h, type: 'command', command: HOOK_COMMAND };
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (!found) { pre.push(hookEntry()); changed = true; }
|
|
510
|
+
hooks.PreToolUse = pre;
|
|
511
|
+
settings.hooks = hooks;
|
|
512
|
+
return { settings, changed };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Does the installed entry still select at least one file-editing tool? The merge deliberately
|
|
516
|
+
// leaves a narrowed `matcher` alone — it is the team's — but a matcher narrowed to nothing (blanked,
|
|
517
|
+
// or pointed at `Bash`) means the guard is installed and never fires, which must not read as healthy.
|
|
518
|
+
// The matcher is a regex the harness tests tool names against, so test it as one; an invalid regex
|
|
519
|
+
// cannot fire either.
|
|
520
|
+
export function hookMatcherFires(settings) {
|
|
521
|
+
const pre = settings?.hooks?.PreToolUse;
|
|
522
|
+
if (!Array.isArray(pre)) return false;
|
|
523
|
+
const tools = HOOK_TOOL_MATCHER.split('|');
|
|
524
|
+
for (const entry of pre) {
|
|
525
|
+
if (!Array.isArray(entry?.hooks) || !entry.hooks.some(OURS)) continue;
|
|
526
|
+
let re;
|
|
527
|
+
try { re = new RegExp(entry.matcher ?? ''); } catch { continue; }
|
|
528
|
+
// An empty matcher matches every tool name in Claude Code, so it is armed, not blank.
|
|
529
|
+
if (!entry.matcher || tools.some((t) => re.test(t))) return true;
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// One harness's settings file as an action. Not a `wiredFileAction`: there is no template to compare
|
|
535
|
+
// bytes against — the file belongs to the team and we own exactly one entry inside it. So it is also
|
|
536
|
+
// deliberately NOT recorded in `.sdlc/managed.json` (recordManagedWrites only records a dest that
|
|
537
|
+
// byte-matches its src); the marker above is its provenance instead.
|
|
538
|
+
function hookSettingsAction(root, ide, relDest) {
|
|
539
|
+
const dest = path.join(root, relDest);
|
|
540
|
+
const raw = exists(dest) ? fs.readFileSync(dest, 'utf8') : null;
|
|
541
|
+
let parsed = null;
|
|
542
|
+
let unreadable = false;
|
|
543
|
+
if (raw !== null) {
|
|
544
|
+
try { parsed = JSON.parse(raw); } catch { unreadable = true; }
|
|
545
|
+
if (!unreadable && (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))) unreadable = true;
|
|
546
|
+
}
|
|
547
|
+
// Scope is the IDE target, item the file under it, so the report reads `.claude/settings.json`
|
|
548
|
+
// once — the same scope the skills for that target are grouped under.
|
|
549
|
+
//
|
|
550
|
+
// `paths` is EMPTY, unlike every other action's: it is the pathspec `yad update --push` stages, and
|
|
551
|
+
// every other entry in that allowlist is a file yad wrote in full. This one the team co-owns, so
|
|
552
|
+
// staging it wholesale would sweep their unrelated working-tree edits into a `chore(yad-update)`
|
|
553
|
+
// commit pushed straight to the default branch, bypassing review. The entry is theirs to commit.
|
|
554
|
+
const base = { scope: ide, item: path.basename(relDest), root, paths: [] };
|
|
555
|
+
// A settings file we cannot READ is never rewritten — not even by `--overwrite-local`.
|
|
556
|
+
//
|
|
557
|
+
// For a managed file, `--overwrite-local` restores the shipped template, which is coherent. Here
|
|
558
|
+
// there is no template: only the parse failed, and everything the file holds — permissions, env,
|
|
559
|
+
// other hooks — is the team's. Synthesizing a replacement from an empty object would discard all of
|
|
560
|
+
// it, and `--overwrite-local` is a generic recovery command someone runs for an unrelated drifted
|
|
561
|
+
// gate script. So this reports `modified` forever and writes nothing; the human fixes the JSON.
|
|
562
|
+
//
|
|
563
|
+
// Warned at PLAN time, not from apply(): reconcile only reaches a `modified` action's apply() with
|
|
564
|
+
// `--overwrite-local`, so a plain `yad check --fix` would print nothing but the generic drift
|
|
565
|
+
// hand — "replace them with `yad update --overwrite-local`" — advice that can never clear this,
|
|
566
|
+
// since this action deliberately writes nothing. The specific reason has to surface either way.
|
|
567
|
+
if (unreadable) {
|
|
568
|
+
warn(`${relDest} does not parse — the ledger guard cannot be wired; fix the JSON, then re-run \`yad check --fix\``);
|
|
569
|
+
return { ...base, status: 'modified', apply: () => {} };
|
|
570
|
+
}
|
|
571
|
+
const { changed } = mergeHookSettings(parsed);
|
|
572
|
+
return {
|
|
573
|
+
...base,
|
|
574
|
+
status: raw === null ? 'missing' : changed ? 'outdated' : 'ok',
|
|
575
|
+
// Re-read at apply() time rather than closing over the merge computed above: setup and reconcile
|
|
576
|
+
// build every action before applying any, so the file may have been written since.
|
|
577
|
+
//
|
|
578
|
+
// A no-op when the entry is already there. `yad setup` re-applies with force:true, which reaches
|
|
579
|
+
// an `ok` action — and an unconditional write would reformat a team's hand-formatted (but valid)
|
|
580
|
+
// settings.json to writeJSON's style on every re-run. Nothing is lost, but the diff noise lands
|
|
581
|
+
// in a committed file we only own one entry of.
|
|
582
|
+
//
|
|
583
|
+
// The re-read is STRICT. `readJSON`'s swallow-and-default would turn a file that became
|
|
584
|
+
// unparseable between plan and apply into `{}` and write the team's whole config away — with no
|
|
585
|
+
// backup, since that is the branch above. Re-check instead, and refuse the same way.
|
|
586
|
+
apply: () => {
|
|
587
|
+
if (!changed && raw !== null) return;
|
|
588
|
+
if (exists(dest)) {
|
|
589
|
+
let current;
|
|
590
|
+
try { current = JSON.parse(fs.readFileSync(dest, 'utf8')); } catch { /* unreadable — refused below */ }
|
|
591
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
592
|
+
warn(`${relDest} does not parse — left untouched; fix the JSON, then re-run \`yad check --fix\``);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
writeJSON(dest, mergeHookSettings(current).settings);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
599
|
+
writeJSON(dest, mergeHookSettings({}).settings);
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Harness-hook wiring on the hub: the guard script plus, per IDE target that defines a hook protocol,
|
|
605
|
+
// the entry that invokes it. Bridge-gated exactly like `hubActions` — with no bridge the ledger is
|
|
606
|
+
// locally owned, the hand-edit the authoring skills describe is CORRECT, and a guard would be wrong.
|
|
607
|
+
export function hookActions(root, ideTargets = ideTargetsFor(root)) {
|
|
608
|
+
const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig));
|
|
609
|
+
if (!isBridgeHub(hub)) return [];
|
|
610
|
+
const ledger = readManagedLedger(root);
|
|
611
|
+
const actions = HOOK_WIRING.map((w) =>
|
|
612
|
+
wiredFileAction('hub', w.dest, asset(w.src), path.join(root, w.dest), { root, exec: !!w.exec, ledger }),
|
|
613
|
+
);
|
|
614
|
+
for (const ide of safeIdeTargetsFor(root, ideTargets)) {
|
|
615
|
+
const relDest = HOOK_SETTINGS[ide];
|
|
616
|
+
if (relDest) actions.push(hookSettingsAction(root, ide, relDest));
|
|
617
|
+
}
|
|
618
|
+
// The two halves must land TOGETHER, so `missing` is relabelled `new` — the same relabel a new
|
|
619
|
+
// first-party skill gets, and for the same reason: `yad update` (--scope=changed) excludes only
|
|
620
|
+
// the literal 'missing'. Without it, an upgrade on a hub that already has a settings.json applies
|
|
621
|
+
// the entry (`outdated`) while skipping the script (`missing`), leaving every file edit firing a
|
|
622
|
+
// PreToolUse command that does not exist — a hook error per edit, and no guarding at all.
|
|
623
|
+
return actions.map(asNewSkill);
|
|
624
|
+
}
|
|
625
|
+
|
|
471
626
|
// Every email the verified-commits gate should accept as a known author: the hub roster's `email`
|
|
472
627
|
// (or `emails`) fields plus hub.json's free-form `verified_authors` list. Lower-cased, deduped,
|
|
473
628
|
// sorted — deterministic so the generated file is drift-checkable like any wired file.
|
package/cli/reconcile.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const readFileSafe = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch {
|
|
|
12
12
|
import { preflightGuardReadiness } from './hubcommit.mjs';
|
|
13
13
|
import { VERSION, PROJECT_FILES, MANAGED_LEDGER, BACKUP_SUFFIX } from './manifest.mjs';
|
|
14
14
|
import {
|
|
15
|
-
moduleActions, repoActions, hubActions, authorsActions,
|
|
15
|
+
moduleActions, repoActions, hubActions, hookActions, authorsActions,
|
|
16
16
|
legacyModuleActions, removedModuleActions, legacyRepoActions, legacyHubActions,
|
|
17
17
|
ideTargetStateFor, recordManagedWrites,
|
|
18
18
|
} from './plan.mjs';
|
|
@@ -48,7 +48,7 @@ export async function reconcile(root, { fix = false, scope = 'all', force = fals
|
|
|
48
48
|
// and purge of skills removed in a later release ('removed': delete the lingering install) ---
|
|
49
49
|
const actions = [
|
|
50
50
|
...moduleActions(root, ideTargets), ...legacyModuleActions(root, ideTargets), ...removedModuleActions(root, ideTargets),
|
|
51
|
-
...hubActions(root), ...legacyHubActions(root),
|
|
51
|
+
...hubActions(root), ...legacyHubActions(root), ...hookActions(root, ideTargets),
|
|
52
52
|
...authorsActions(root, registry.repos),
|
|
53
53
|
];
|
|
54
54
|
if (ideState.needsRepair) {
|
package/cli/setup.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from './lib.mjs';
|
|
9
9
|
import { VERSION, IDE_TARGETS, PROJECT_FILES, DESIGN_TOOLS, DESIGN_PRIMARY, TESTING_TOOLS, TESTING_PRIMARY, LEARNING_TOOLS, LEARNING_PRIMARY } from './manifest.mjs';
|
|
10
10
|
import {
|
|
11
|
-
moduleActions, repoActions, hubActions, authorsActions,
|
|
11
|
+
moduleActions, repoActions, hubActions, hookActions, authorsActions,
|
|
12
12
|
legacyModuleActions, removedModuleActions, legacyRepoActions, legacyHubActions,
|
|
13
13
|
safeIdeTargetsFor, detectedIdeTargetStateFor, recordManagedWrites,
|
|
14
14
|
} from './plan.mjs';
|
|
@@ -734,6 +734,15 @@ export async function runSetup(root, opts = {}) {
|
|
|
734
734
|
wired.push(...hubWiring);
|
|
735
735
|
}
|
|
736
736
|
applyActions(legacyHubActions(root), { force: true });
|
|
737
|
+
// the hub, locally: the harness ledger guard, so an agent is refused the CI-owned ledger write at
|
|
738
|
+
// the moment it tries it rather than by a failed pipeline later (#171). Bridge-gated like the CI
|
|
739
|
+
// above — with no bridge the ledger is locally owned and the guard would be wrong.
|
|
740
|
+
const hookWiring = hookActions(root, ideTargets);
|
|
741
|
+
if (hookWiring.length) {
|
|
742
|
+
log(` ${c.bold('hub')} ${c.dim('(agent ledger guard)')}`);
|
|
743
|
+
applyActions(hookWiring, { force: true });
|
|
744
|
+
wired.push(...hookWiring);
|
|
745
|
+
}
|
|
737
746
|
// After every write to a managed path has landed (including the legacy renames), so the recorded
|
|
738
747
|
// sha is the file's final state.
|
|
739
748
|
recordManagedWrites(wired);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.1",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -94,7 +94,7 @@ build:
|
|
|
94
94
|
spec_layout: speckit # follow Spec Kit's native spec/plan/tasks layout
|
|
95
95
|
speckit_ceremony: [specify, clarify, plan, analyze, checklist, tasks] # heavy run, once per story per repo
|
|
96
96
|
# Step B (yad-implement) — the light per-task loop. One atomic task = one branch = one PR/MR.
|
|
97
|
-
branch_convention: "feat/<story-id>-<task-id>-<short-slug>" # e.g. feat/EP-
|
|
97
|
+
branch_convention: "feat/<story-id>-<task-id>-<short-slug>" # e.g. feat/EP-checkout-S01-T01-create-order
|
|
98
98
|
commit_task_trailer: "Task: <story-id>-<task-id>" # final commit trailer; anchors the spec-link check (Step C)
|
|
99
99
|
contract_change_trailer: "Contract-Change: yes" # ONLY when the locked contract surface is touched (routes back to architecture gate)
|
|
100
100
|
# Commit subject + PR/MR title style (Conventional Commits — see CONTRIBUTING.md). PRs are squash-merged,
|
|
@@ -62,7 +62,7 @@ roadmap. **Optional & non-blocking:** if there is no discovery, or it has not ye
|
|
|
62
62
|
|
|
63
63
|
### Step 3 — Generate the Epic ID (engine-assigned, never by hand)
|
|
64
64
|
Derive `EP-<slug>` where `slug` is **2–4 lowercase words joined by hyphens**, drawn from the idea
|
|
65
|
-
(e.g. `EP-
|
|
65
|
+
(e.g. `EP-checkout`). Lowercase except the fixed `EP` prefix. `EP-discovery` is **reserved**
|
|
66
66
|
for the project front-zero — never use it for a feature. **The ID is assigned once and
|
|
67
67
|
never renamed** — renaming breaks every downstream link (build plan §6b). Check
|
|
68
68
|
`{project-root}/epics/` for collisions; if the slug exists, append a distinguishing word.
|
|
@@ -65,6 +65,11 @@ and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the sk
|
|
|
65
65
|
default branch the guard is absolute again. Runs in `yad-hub-checks`
|
|
66
66
|
alongside `verified-commits` (which waives the allowlist for the bot but still requires its
|
|
67
67
|
signature). See `yad-hub-bridge`.
|
|
68
|
+
- `templates/hooks/ledger-guard.sh` → **hub-only** agent guardrail, active **only in bridge mode**
|
|
69
|
+
(the same `isBridgeHub` predicate). Not a CI gate: it is a **harness hook** that refuses an agent's
|
|
70
|
+
edit to the CI-owned ledger at the moment it is attempted and names `yad gate open` instead — the
|
|
71
|
+
local half of `checks/ledger-guard.sh` (#171). Installed to `<hub>/hooks/ledger-guard.sh` with the
|
|
72
|
+
`PreToolUse` entry in `.claude/settings.json`. Fails OPEN; see "Step 2b" below.
|
|
68
73
|
- `templates/github/yad-verified-commits.yml` + `templates/gitlab/yad-verified-commits.gitlab-ci.yml`
|
|
69
74
|
→ the standalone hub-side verified-commits CI (installed by `yad check --fix` with the hub wiring)
|
|
70
75
|
- `templates/github/yad-checks.yml` → installs to `.github/workflows/yad-checks.yml` (marked `# yad-managed: yad-checks`)
|
|
@@ -142,6 +147,43 @@ Commit the wiring on the repo's default branch (it is shared infrastructure, not
|
|
|
142
147
|
**The hub is wired the same way.** `repo: hub` wires the hub repo itself (platform from `.sdlc/hub.json`)
|
|
143
148
|
with a hub-flavored gate set — see "Wiring the hub" in `references/check-gates.md`.
|
|
144
149
|
|
|
150
|
+
**The hub also gets the agent guardrail** (see below): `templates/hooks/ledger-guard.sh` →
|
|
151
|
+
`<hub>/hooks/ledger-guard.sh`, plus the `PreToolUse` entry in `.claude/settings.json`. `yad setup`
|
|
152
|
+
and `yad check --fix` install both; there is nothing to do by hand.
|
|
153
|
+
|
|
154
|
+
### Step 2b — the agent guardrail (harness hooks, bridge mode only)
|
|
155
|
+
The CI gates speak at CI time. That is too late for one failure the field kept hitting (#171): in
|
|
156
|
+
bridge mode the gate ledger is **CI-owned**, so an agent that hand-edits
|
|
157
|
+
`epics/*/.sdlc/state.json` only finds out twenty minutes later, from a `ledger-guard` FAIL with
|
|
158
|
+
nothing connecting cause to effect — and by then the write has to be reverted before the review
|
|
159
|
+
PR/MR can go green.
|
|
160
|
+
|
|
161
|
+
`hooks/ledger-guard.sh` is the local half of that same rule. It runs as a **harness hook** before a
|
|
162
|
+
file-editing tool call and refuses the write up front, naming the command that owns the transition
|
|
163
|
+
(`yad gate open`), so the agent corrects itself instead of failing a pipeline.
|
|
164
|
+
|
|
165
|
+
- **Harness-agnostic by contract.** The script only locates `yad` and hands the tool payload to
|
|
166
|
+
`yad hook ledger-guard`: **stdin** is a JSON tool-call payload, **exit 0** allows, **exit 2**
|
|
167
|
+
denies with the reason on stderr. Claude Code's `PreToolUse` protocol is exactly that, so no
|
|
168
|
+
adapter logic is needed; another harness needs only those two exit codes.
|
|
169
|
+
- **Same scope as the CI gate**, deliberately: guarded are `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json`
|
|
170
|
+
and `epics/*/reviews/*.md` (at the gate's own glob depth, which spans `/`); exempt are
|
|
171
|
+
`contract-lock.json`, `change.json`, and every artifact. A **new** epic's ledger is exempt too —
|
|
172
|
+
creation, not mutation (#162), decided by listing the epics the **base ref** carries (an
|
|
173
|
+
`origin/` ref, case-folded slugs), never by looking at the working tree.
|
|
174
|
+
- **A no-op without the bridge.** There the ledger is locally owned and the hand-edit the authoring
|
|
175
|
+
skills describe is *correct*, so nothing is wired and nothing is blocked.
|
|
176
|
+
- **It fails OPEN** — no `yad`, no hub, an unreadable config, an unparseable payload all ALLOW, with
|
|
177
|
+
a note on stderr. `ledger-guard` in CI fails *closed* and remains the authority. `YAD_HOOK_DISABLE=1`
|
|
178
|
+
skips one command.
|
|
179
|
+
- **Known gaps** (both caught by the CI gate instead): a `Bash` tool call (`sed -i epics/…`) is not
|
|
180
|
+
intercepted — matching it would mean parsing shell; and the hook arms sessions **rooted at the
|
|
181
|
+
hub**, since a harness reads hooks from its own project root — a session opened at the workspace
|
|
182
|
+
above the hub never loads the hub's `.claude/settings.json`.
|
|
183
|
+
|
|
184
|
+
`yad doctor` reports the guardrail as `agent ledger guard wired` / `not wired` on a bridge hub.
|
|
185
|
+
See `references/check-gates.md` §"The agent guardrail".
|
|
186
|
+
|
|
145
187
|
### Step 3 — `run` (run the gates now)
|
|
146
188
|
From inside the repo, run each gate against `base` and report PASS/FAIL per gate:
|
|
147
189
|
```
|
|
@@ -366,6 +366,112 @@ title + the code task template), so a PR that changes the hub's own workflows/ch
|
|
|
366
366
|
`templates/gitlab/yad-hub-checks.gitlab-ci.yml` → `.gitlab/ci/yad-hub-checks.yml` + its one include
|
|
367
367
|
line). Code repos run the same three with `--profile code` inside the main `yad-checks` workflow.
|
|
368
368
|
|
|
369
|
+
## The agent guardrail (`templates/hooks/ledger-guard.sh` + `yad hook ledger-guard`)
|
|
370
|
+
|
|
371
|
+
Not a CI gate — a **harness hook**, and the only piece of yadflow that runs *inside* an agent's tool
|
|
372
|
+
loop. It exists because of the gap #171 reported: `checks/ledger-guard.sh` is correct and blocking,
|
|
373
|
+
but it speaks at CI time. An agent that hand-edits `epics/*/.sdlc/state.json` in bridge mode learns
|
|
374
|
+
twenty minutes later, from a FAIL with nothing connecting cause to effect, and by then the write must
|
|
375
|
+
be reverted before the review PR/MR can go green.
|
|
376
|
+
|
|
377
|
+
**Contract** — deliberately not Claude-Code-shaped:
|
|
378
|
+
|
|
379
|
+
| | |
|
|
380
|
+
|---|---|
|
|
381
|
+
| stdin | a harness tool-call payload as JSON (optional; `--path <p>` works instead) |
|
|
382
|
+
| exit 0 | allow |
|
|
383
|
+
| exit 2 | deny — the reason is on stderr, for the agent to read |
|
|
384
|
+
|
|
385
|
+
Claude Code's `PreToolUse` protocol is exactly that (exit 2 blocks the call and feeds stderr back to
|
|
386
|
+
the model), so `.claude/settings.json` wires it with no adapter logic. Any harness that can run a
|
|
387
|
+
command and read those two exit codes can use the same script.
|
|
388
|
+
|
|
389
|
+
**Layering.** `hooks/ledger-guard.sh` is only the adapter: it locates `yad` (`$YAD_BIN` → the hub's
|
|
390
|
+
`node_modules/yadflow` → `PATH` → `npx --no-install`) and passes the payload to `yad hook
|
|
391
|
+
ledger-guard`, which holds the decision. So the wiring never hard-codes an install path, and the
|
|
392
|
+
logic is unit-tested (`cli/hook.mjs`, `cli/test.mjs`) instead of living in bash.
|
|
393
|
+
|
|
394
|
+
**Scope — identical to the CI gate, on purpose**, down to the details that decide the hard cases:
|
|
395
|
+
|
|
396
|
+
- Guarded: `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json` and `epics/*/reviews/*.md`.
|
|
397
|
+
Exempt: `contract-lock.json` (artifact-side), `change.json`, every artifact.
|
|
398
|
+
- **Depth matches the gate's globs.** Its arms are bash `case` patterns, and a bash `*` spans `/`, so
|
|
399
|
+
`epics/EP-a/nested/.sdlc/state.json` is guarded there — and here. Being stricter locally would let
|
|
400
|
+
a path through that CI blocks.
|
|
401
|
+
- **The seed carve-out reads the base ref, not the working tree** (#162): the epics whose
|
|
402
|
+
`state.json` the base carries are listed once with `ls-tree`, and an epic absent from that list is
|
|
403
|
+
a creation. Never a `<rev>:<path>` probe — that spec resolves from the repository top level and
|
|
404
|
+
`-C` does not re-anchor it, so a hub in a subdirectory of its repo would miss every time and the
|
|
405
|
+
guard would allow everything, silently.
|
|
406
|
+
- **The base is an `origin/` ref** — `origin/<default_branch>`, then the remote's published default,
|
|
407
|
+
then `origin/main`, the gate's own order. Never a bare local branch: `git fetch` does not
|
|
408
|
+
fast-forward one, so a stale trunk would report a merged epic's ledger as absent and wave a real
|
|
409
|
+
mutation through. If none resolves, the answer is *unknown* and unknown allows.
|
|
410
|
+
- **Slugs are case-folded**, as the gate folds them. On a case-insensitive filesystem `epics/ep-x/…`
|
|
411
|
+
and `epics/EP-X/…` are the same file, so a byte-exact compare would let a mutation be laundered as
|
|
412
|
+
a creation.
|
|
413
|
+
- Bridge-gated by the same `isBridgeHub` predicate the CLI and the wiring read (#186): without the
|
|
414
|
+
bridge the ledger is locally owned, the hand-edit the authoring skills describe is correct, and
|
|
415
|
+
nothing is wired or blocked.
|
|
416
|
+
|
|
417
|
+
**It fails OPEN, and that asymmetry is the design.** No `yad` on PATH, no hub above the edited path,
|
|
418
|
+
an unreadable `hub.json`, an unparseable payload, a `yad` that errors — every one of them ALLOWS,
|
|
419
|
+
with a note on stderr. A local guardrail that failed closed would brick an agent's ability to edit
|
|
420
|
+
anything the moment an install went sideways. The CI gate fails **closed** and is what actually
|
|
421
|
+
protects the ledger; this only shortens the feedback loop. `YAD_HOOK_DISABLE=1` skips one command.
|
|
422
|
+
|
|
423
|
+
**Known gaps** — both fall through to the CI gate, which is why it stays the authority:
|
|
424
|
+
|
|
425
|
+
- A `Bash` tool call (`sed -i epics/…`) is not intercepted; matching it would mean parsing shell for
|
|
426
|
+
write intent.
|
|
427
|
+
- The hook arms sessions **rooted at the hub**. A harness loads hooks from its own project root, so a
|
|
428
|
+
session opened at the *workspace* (`project/`, with the hub at `project/product/`) never reads the
|
|
429
|
+
hub's `.claude/settings.json` and the guard does not fire there — even though the decision itself
|
|
430
|
+
resolves the hub correctly from any path. In that layout, open the session at the hub, or copy the
|
|
431
|
+
entry into the workspace's own settings (the command's `$CLAUDE_PROJECT_DIR` would then need the
|
|
432
|
+
hub-relative path).
|
|
433
|
+
|
|
434
|
+
**Wiring** (installed by `yad setup` / `yad check --fix`, bridge hubs only):
|
|
435
|
+
|
|
436
|
+
| Path | Owner |
|
|
437
|
+
|---|---|
|
|
438
|
+
| `<hub>/hooks/ledger-guard.sh` | fully managed — drift-checked and recorded in `.sdlc/managed.json` like any gate script |
|
|
439
|
+
| `<hub>/.claude/settings.json` | **one entry**, merged additively into `hooks.PreToolUse`. See below. |
|
|
440
|
+
|
|
441
|
+
The settings file is the team's, so the rules around that one entry are deliberately conservative:
|
|
442
|
+
|
|
443
|
+
- **Ownership is an exact command match** — the current spelling or a documented past one — never a
|
|
444
|
+
substring. A team keeping its own wrapper at `.claude/hooks/ledger-guard.sh` would otherwise have
|
|
445
|
+
their hook silently rewritten to ours, on the `outdated` path that takes no backup. Matching
|
|
446
|
+
exactly means the worst case is a second entry (the guard runs twice, harmlessly).
|
|
447
|
+
- **The command is quoted** (`"$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh"`) because the harness runs
|
|
448
|
+
it through a shell: unquoted, a project path containing a space word-splits and the guard is
|
|
449
|
+
silently off while `check` and `doctor` still call it wired.
|
|
450
|
+
- **A file that does not parse is never rewritten** — not even by `--overwrite-local`. For a managed
|
|
451
|
+
file that flag restores a shipped template; here there is none, and everything in the file is the
|
|
452
|
+
team's. It reports `modified` until a human fixes the JSON.
|
|
453
|
+
- **A `matcher` the team narrowed is left as they set it** — but `yad doctor` warns when it no longer
|
|
454
|
+
selects any file-editing tool, so an installed-but-dead guard cannot pass for healthy.
|
|
455
|
+
- **It is never staged by `yad update --push`.** Every other path in that allowlist is a file yad
|
|
456
|
+
wrote in full; this one would sweep the team's unrelated edits into a `chore` commit pushed
|
|
457
|
+
straight to the default branch.
|
|
458
|
+
- **Both halves land together.** The script and the entry ride `yad update` as one: applying the
|
|
459
|
+
entry without the script it points at would fire a missing command on every file edit.
|
|
460
|
+
|
|
461
|
+
`.claude` is the only IDE target wired: it is the only one with a defined hook protocol. Other
|
|
462
|
+
targets get the script, and the contract above is what they would wire by hand.
|
|
463
|
+
|
|
464
|
+
`yad doctor` reports the guard on a bridge hub, and distinguishes the three states that matter — it
|
|
465
|
+
reads the same persisted `ideTargets` the wiring reads, so every gap it names is one the command it
|
|
466
|
+
names can actually close:
|
|
467
|
+
|
|
468
|
+
| State | Report | Remedy |
|
|
469
|
+
|---|---|---|
|
|
470
|
+
| script + entry present, matcher live | `agent ledger guard wired` | — |
|
|
471
|
+
| either half absent | `not wired: <what>` | `yad check --fix` |
|
|
472
|
+
| present but the matcher no longer selects a file-editing tool | `installed but its matcher no longer selects file edits` | restore the matcher — it is wired and never fires |
|
|
473
|
+
| the settings file does not parse | `cannot be wired — … does not parse` | fix the JSON by hand; yad never rewrites one it cannot parse, so nothing else clears it |
|
|
474
|
+
|
|
369
475
|
## Running by hand (Phase 3 is manual)
|
|
370
476
|
|
|
371
477
|
From inside the code repo, against the PR/MR base (e.g. `master`). For the gates that take one, the
|
|
@@ -383,15 +489,15 @@ bash checks/commit-message.sh --profile code master
|
|
|
383
489
|
# pr-title / pr-template validate the actual PR/MR metadata (in CI they come from the event payload).
|
|
384
490
|
# By hand, pass the title, and a FILE holding the PR/MR description (the rendered/filled body, not the
|
|
385
491
|
# template source):
|
|
386
|
-
bash checks/pr-title.sh --profile code "feat: add the
|
|
492
|
+
bash checks/pr-title.sh --profile code "feat: add the order endpoint"
|
|
387
493
|
# save the PR/MR description to a file first (e.g. `gh pr view <n> --json body -q .body > /tmp/pr-body.md`)
|
|
388
494
|
bash checks/pr-template.sh --profile code /tmp/pr-body.md
|
|
389
495
|
```
|
|
390
496
|
|
|
391
|
-
## Proven behavior (demo: `demo-repos/backend`, story EP-
|
|
497
|
+
## Proven behavior (demo: `demo-repos/backend`, story EP-checkout-S01)
|
|
392
498
|
|
|
393
499
|
- **Good PR** (task branch with a `Task:` trailer, no surface change, passing tests) → all three **PASS**.
|
|
394
500
|
- **Bad PR A** (a code change committed with **no** `Task:` trailer) → spec-link **FAILS**.
|
|
395
|
-
- **Bad PR B** (edits `specs/.../contracts/
|
|
501
|
+
- **Bad PR B** (edits `specs/.../contracts/orders.md` to widen the surface, with a `Task:` trailer
|
|
396
502
|
but **no** `Contract-Change`) → spec-link passes, contract-check **FAILS** and routes back to the
|
|
397
503
|
architecture gate.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ledger-guard HARNESS HOOK — the local half of the CI gate of the same name (#171).
|
|
3
|
+
#
|
|
4
|
+
# The gate ledger is CI-owned in bridge mode: `checks/ledger-guard.sh` rejects any non-bot commit
|
|
5
|
+
# that changes `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json` or `epics/*/reviews/*.md`.
|
|
6
|
+
# This hook says so at the moment an agent tries the edit, instead of twenty minutes later in a
|
|
7
|
+
# failed pipeline, and names the command that owns the transition (`yad gate open`).
|
|
8
|
+
#
|
|
9
|
+
# This file is only the ADAPTER. It locates `yad` and hands the tool-call payload to
|
|
10
|
+
# `yad hook ledger-guard`, which holds the decision — so the wiring never hard-codes an install path
|
|
11
|
+
# and the logic stays testable. The contract it passes through:
|
|
12
|
+
#
|
|
13
|
+
# stdin the harness's tool-call payload as JSON (optional)
|
|
14
|
+
# exit 0 allow
|
|
15
|
+
# exit 2 deny, reason on stderr
|
|
16
|
+
#
|
|
17
|
+
# Wired for Claude Code as a `PreToolUse` hook in `.claude/settings.json` (`yad check --fix` writes
|
|
18
|
+
# that entry). Any harness that can run a command and read those two exit codes can use it.
|
|
19
|
+
#
|
|
20
|
+
# FAIL-OPEN: if no `yad` can be found, this ALLOWS and says why on stderr. A guardrail that blocked
|
|
21
|
+
# every edit the moment an install went sideways would be worse than the problem. The CI gate fails
|
|
22
|
+
# CLOSED and is what actually protects the ledger.
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
# The hub root is this script's grandparent — hooks/ledger-guard.sh — so the resolution below does
|
|
26
|
+
# not depend on the harness's working directory.
|
|
27
|
+
HOOK_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
HUB_ROOT="$(dirname -- "$HOOK_DIR")"
|
|
29
|
+
|
|
30
|
+
# Resolution order, cheapest and most specific first: an explicit override, then the copy installed
|
|
31
|
+
# in this hub, then whatever is on PATH, then a network-free npx. `--no-install` matters — a hook
|
|
32
|
+
# runs on every tool call and must never pause an agent to download a package.
|
|
33
|
+
CMD=()
|
|
34
|
+
_yad_bin="${YAD_BIN:-}"
|
|
35
|
+
if [ -n "${_yad_bin//[[:space:]]/}" ]; then
|
|
36
|
+
# Split deliberately: YAD_BIN is commonly an interpreter + script ("node /path/to/yad.mjs").
|
|
37
|
+
# The whitespace-stripped test above matters: a YAD_BIN of only spaces would leave CMD empty, and
|
|
38
|
+
# macOS's bash 3.2 treats "${CMD[@]}" on an empty array as an unbound variable under `set -u` —
|
|
39
|
+
# aborting the script with a confusing 127 instead of taking one of the branches below.
|
|
40
|
+
read -r -a CMD <<< "$_yad_bin"
|
|
41
|
+
elif [ -f "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
42
|
+
CMD=(node "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs")
|
|
43
|
+
elif command -v yad >/dev/null 2>&1; then
|
|
44
|
+
CMD=(yad)
|
|
45
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
46
|
+
CMD=(npx --no-install yadflow)
|
|
47
|
+
else
|
|
48
|
+
echo " • yad hook: no \`yad\` on PATH and none installed in $HUB_ROOT — allowing (install yadflow to re-arm the ledger guard)" >&2
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Belt and braces for bash 3.2's empty-array-is-unbound rule: every branch above sets CMD, but an
|
|
53
|
+
# unexpanded array under `set -u` would abort the script rather than allow, so check before using it.
|
|
54
|
+
if [ "${#CMD[@]}" -eq 0 ]; then
|
|
55
|
+
echo " • yad hook: could not resolve a \`yad\` to run — allowing" >&2
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Run it rather than `exec`, so the exit code can be mapped. ONLY an explicit deny (2) blocks: a
|
|
60
|
+
# `yad` that is present but cannot run — an `npx --no-install` with no yadflow to find, a crash, a
|
|
61
|
+
# broken install — must not read as a refusal. Fail-open is the whole stance of this hook; the CI
|
|
62
|
+
# gate is what fails closed.
|
|
63
|
+
"${CMD[@]}" hook ledger-guard "$@"
|
|
64
|
+
rc=$?
|
|
65
|
+
[ "$rc" -eq 2 ] && exit 2
|
|
66
|
+
if [ "$rc" -ne 0 ]; then
|
|
67
|
+
echo " • yad hook: \`${CMD[*]} hook ledger-guard\` exited $rc — allowing (run \`yad doctor\` to check the install)" >&2
|
|
68
|
+
fi
|
|
69
|
+
exit 0
|
|
@@ -17,7 +17,7 @@ root, not under any `epics/EP-<slug>/.sdlc/`.
|
|
|
17
17
|
"tool": "deeptutor", // deeptutor | <adapter id> | none (harness-native)
|
|
18
18
|
"provider": "deeptutor-cli", // the concrete CLI: deeptutor-cli | null
|
|
19
19
|
"version": "1.4.5", // CLI version reported at detect time; null if absent
|
|
20
|
-
"kb": "yadflow-
|
|
20
|
+
"kb": "yadflow-checkout", // grounded knowledge-base name; null if not built
|
|
21
21
|
"kb_sources": ["epic.md", "architecture.md", "contract.md", "ui-design.md", "stories/", "code-context/*/code-map.md"],
|
|
22
22
|
"auth": "user", // ALWAYS the user's own DeepTutor config / LLM keys — never a token
|
|
23
23
|
"connectedAt": "2026-06-14", // first connect (YYYY-MM-DD)
|
|
@@ -36,13 +36,13 @@ without the `{ epic, ships }` wrapper):
|
|
|
36
36
|
|
|
37
37
|
```json
|
|
38
38
|
{
|
|
39
|
-
"epic": "EP-
|
|
39
|
+
"epic": "EP-checkout",
|
|
40
40
|
"ships": [
|
|
41
41
|
{
|
|
42
|
-
"story": "EP-
|
|
42
|
+
"story": "EP-checkout-S01",
|
|
43
43
|
"task": "T01",
|
|
44
44
|
"repo": "backend",
|
|
45
|
-
"branch": "feat/EP-
|
|
45
|
+
"branch": "feat/EP-checkout-S01-T01-create-order",
|
|
46
46
|
"pr": "<url|#|local>",
|
|
47
47
|
"mergeCommit": "<sha>",
|
|
48
48
|
"gates": ["spec-link", "contract-check", "build-test-lint"],
|
package/skills/yad-epic/SKILL.md
CHANGED
|
@@ -83,7 +83,7 @@ unchanged — do not consume an unapproved roadmap. After seeding the epic, the
|
|
|
83
83
|
### Step 3 — Generate the Epic ID (engine-assigned, never by hand) — analysis-skipped only
|
|
84
84
|
*(Skip when analysis ran — the ID was already assigned by `yad-analysis`.)*
|
|
85
85
|
Derive `EP-<slug>` where `slug` is **2–4 lowercase words joined by hyphens**, drawn from the idea
|
|
86
|
-
(e.g. `EP-
|
|
86
|
+
(e.g. `EP-checkout`). Lowercase except the fixed `EP` prefix. `EP-discovery` is **reserved**
|
|
87
87
|
for the project front-zero — never use it for a feature. **The ID is assigned once and
|
|
88
88
|
never renamed** — renaming breaks every downstream link (build plan §6b).
|
|
89
89
|
Check `{project-root}/epics/` for collisions; if the slug exists, append a distinguishing word.
|
|
@@ -416,7 +416,7 @@ only re-authored steps run. The seeder sets `currentStep` to the first re-author
|
|
|
416
416
|
```json
|
|
417
417
|
{ "id": "architecture", "type": "author", "artifact": "architecture.md",
|
|
418
418
|
"assistance": "review", "automation": "human_approve", "locked": true,
|
|
419
|
-
"status": "done", "inherited": true, "inheritedFrom": "EP-
|
|
419
|
+
"status": "done", "inherited": true, "inheritedFrom": "EP-checkout",
|
|
420
420
|
"boundHash": "sha256:…", "risk_tags": [] }
|
|
421
421
|
```
|
|
422
422
|
|
|
@@ -432,7 +432,7 @@ only re-authored steps run. The seeder sets `currentStep` to the first re-author
|
|
|
432
432
|
|
|
433
433
|
```json
|
|
434
434
|
{ "artifact": "architecture.md", "step": "architecture-review", "status": "inherited",
|
|
435
|
-
"from": "EP-
|
|
435
|
+
"from": "EP-checkout", "boundHash": "sha256:…", "date": "<YYYY-MM-DD>" }
|
|
436
436
|
```
|
|
437
437
|
|
|
438
438
|
## The pointer-lock — `contract-lock.json` in a change-epic
|
|
@@ -443,7 +443,7 @@ no `contract.md` in the child to edit, so the surface physically cannot drift.
|
|
|
443
443
|
|
|
444
444
|
```json
|
|
445
445
|
{ "artifact": "contract.md", "hash": "sha256:<parent hash, verbatim>", "lockedAt": "<date>",
|
|
446
|
-
"inheritedFrom": "EP-
|
|
446
|
+
"inheritedFrom": "EP-checkout", "ref": "../../EP-checkout/.sdlc/contract-lock.json" }
|
|
447
447
|
```
|
|
448
448
|
|
|
449
449
|
Omitting `architecture` from `inherits` (depth `contract-surface`) is what triggers a **real re-lock**:
|
|
@@ -455,9 +455,9 @@ architecture gate" with "open a contract-surface change-epic" — one mechanism,
|
|
|
455
455
|
Intake + triage record, one per change/defect/hotfix epic (sibling of `approvals.json`).
|
|
456
456
|
|
|
457
457
|
```json
|
|
458
|
-
{ "epicId": "EP-
|
|
458
|
+
{ "epicId": "EP-checkout-queue-filter", "thread": "EP-checkout", "parent": "EP-checkout",
|
|
459
459
|
"kind": "defect", "depth": "defect-fix", "intakeBy": "alice", "intakeDate": "<YYYY-MM-DD>",
|
|
460
|
-
"title": "Pending queue returns
|
|
460
|
+
"title": "Pending queue returns fulfilled orders", "description": "…",
|
|
461
461
|
"affectedArtifacts": ["stories", "test-cases"],
|
|
462
462
|
"reauthors": ["stories", "test-cases"], "inherits": ["epic", "architecture", "contract", "ui-design"],
|
|
463
463
|
"defect": { "origin": "production", "severity": "sev2", "escape_stage": "test-cases",
|
|
@@ -476,7 +476,7 @@ sharing `thread` and read each `change.json`; there is no duplicated thread regi
|
|
|
476
476
|
Append-only ledger of hotfix ship-first debt (a hotfix shipped code before its front gates approved).
|
|
477
477
|
|
|
478
478
|
```json
|
|
479
|
-
[ { "thread": "EP-
|
|
479
|
+
[ { "thread": "EP-checkout", "epicId": "EP-checkout-hotfix-x", "openedDate": "<date>",
|
|
480
480
|
"reason": "prod outage", "requires": ["artifacts-updated", "regression-test"],
|
|
481
481
|
"status": "open", "paidDate": null, "paidBy": null,
|
|
482
482
|
"evidence": { "artifacts": [], "regressionTest": "" } } ]
|
|
@@ -87,7 +87,9 @@ Install the hub CI that turns the human **merge** into a `yad gate ci` run, with
|
|
|
87
87
|
writer** of the ledger. There is no pre-merge CI write — during review the platform PR/MR is the
|
|
88
88
|
source of truth (native approvals + threads). On merge, CI re-reads approvals from the platform,
|
|
89
89
|
advances the step, and flips the artifact `status:` on the **default branch** (the only place CI ever
|
|
90
|
-
commits). Also install the `ledger-guard` check (yad-checks) so humans cannot commit gate-state files
|
|
90
|
+
commits). Also install the `ledger-guard` check (yad-checks) so humans cannot commit gate-state files,
|
|
91
|
+
and its local counterpart `hooks/ledger-guard.sh` — the harness hook that refuses an **agent** the
|
|
92
|
+
same write at the moment it tries it, instead of letting it surface as a CI failure later (#171).
|
|
91
93
|
Revoke-on-change is enforced at merge: on **GitHub** in code (an approval whose commit ≠ the merged
|
|
92
94
|
head is dropped — no setting needed); on **GitLab** it has no per-approval commit SHA, so enabling the
|
|
93
95
|
platform's **"remove all approvals when commits are added to the source branch"** is **required** for
|