yadflow 4.0.0-next.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +699 -0
  2. package/README.md +85 -22
  3. package/bin/commands.mjs +3 -1
  4. package/bin/yad.mjs +183 -29
  5. package/cli/capture.mjs +4 -2
  6. package/cli/checkpoint.mjs +10 -10
  7. package/cli/claims.mjs +4 -4
  8. package/cli/commit.mjs +48 -12
  9. package/cli/docs.mjs +15 -13
  10. package/cli/doctor.mjs +557 -141
  11. package/cli/epic-state.mjs +41 -18
  12. package/cli/errors.mjs +3 -2
  13. package/cli/find-product.mjs +197 -0
  14. package/cli/fold.mjs +4 -4
  15. package/cli/gate-local.mjs +13 -13
  16. package/cli/gate.mjs +111 -103
  17. package/cli/history.mjs +13 -13
  18. package/cli/hook.mjs +201 -30
  19. package/cli/index-command.mjs +6 -6
  20. package/cli/lib.mjs +91 -29
  21. package/cli/manifest.mjs +171 -104
  22. package/cli/migrate.mjs +208 -17
  23. package/cli/mode.mjs +22 -22
  24. package/cli/next.mjs +6 -6
  25. package/cli/openpr.mjs +32 -24
  26. package/cli/people.mjs +22 -10
  27. package/cli/plan.mjs +718 -45
  28. package/cli/platform.mjs +13 -7
  29. package/cli/product-index.mjs +4 -4
  30. package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
  31. package/cli/protection.mjs +19 -9
  32. package/cli/reconcile.mjs +98 -23
  33. package/cli/repo-publish.mjs +22 -16
  34. package/cli/repo.mjs +128 -33
  35. package/cli/report.mjs +4 -2
  36. package/cli/review.mjs +26 -19
  37. package/cli/setup.mjs +105 -36
  38. package/cli/ship.mjs +1 -1
  39. package/cli/skip.mjs +4 -4
  40. package/cli/tidy.mjs +7 -7
  41. package/cli/update-commit.mjs +22 -19
  42. package/cli/workspace.mjs +495 -0
  43. package/package.json +7 -4
  44. package/skills/sdlc/config.yaml +14 -13
  45. package/skills/sdlc/module-help.csv +8 -8
  46. package/skills/yad-analysis/SKILL.md +6 -4
  47. package/skills/yad-architecture/SKILL.md +4 -3
  48. package/skills/yad-backfill/SKILL.md +11 -5
  49. package/skills/yad-backfill/references/backfill.md +33 -3
  50. package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
  51. package/skills/yad-change/SKILL.md +1 -1
  52. package/skills/yad-checks/SKILL.md +48 -27
  53. package/skills/yad-checks/references/check-gates.md +213 -53
  54. package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
  55. package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
  56. package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
  57. package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
  58. package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
  59. package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
  60. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
  61. package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
  62. package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
  63. package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
  64. package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
  65. package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
  66. package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
  67. package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
  68. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
  69. package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
  70. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
  71. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
  72. package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
  73. package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
  74. package/skills/yad-commit/SKILL.md +18 -7
  75. package/skills/yad-connect-design/SKILL.md +1 -1
  76. package/skills/yad-connect-design/references/design-registry.md +1 -1
  77. package/skills/yad-connect-docs/SKILL.md +16 -11
  78. package/skills/yad-connect-docs/references/docs-registry.md +8 -8
  79. package/skills/yad-connect-learning/SKILL.md +1 -1
  80. package/skills/yad-connect-learning/references/learning-registry.md +1 -1
  81. package/skills/yad-connect-repos/SKILL.md +31 -8
  82. package/skills/yad-connect-repos/references/code-context.md +1 -1
  83. package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
  84. package/skills/yad-connect-repos/references/repos-registry.md +1 -1
  85. package/skills/yad-connect-testing/SKILL.md +1 -1
  86. package/skills/yad-connect-testing/references/testing-registry.md +1 -1
  87. package/skills/yad-discovery/SKILL.md +2 -2
  88. package/skills/yad-discovery/references/discovery-schema.md +1 -1
  89. package/skills/yad-docs/SKILL.md +1 -1
  90. package/skills/yad-docs/references/data-mapping.md +1 -1
  91. package/skills/yad-docs/templates/app/package-lock.json +8 -8
  92. package/skills/yad-docs/templates/app/package.json +1 -1
  93. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
  94. package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
  95. package/skills/yad-engineer-review/SKILL.md +2 -2
  96. package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
  97. package/skills/yad-epic/SKILL.md +6 -4
  98. package/skills/yad-epic/references/state-schema.md +13 -12
  99. package/skills/yad-open-pr/SKILL.md +1 -1
  100. package/skills/yad-pair-review/SKILL.md +5 -5
  101. package/skills/yad-pair-review/references/session-state.md +2 -2
  102. package/skills/yad-pr-template/SKILL.md +14 -12
  103. package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
  104. package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
  105. package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
  106. package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
  107. package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
  108. package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
  109. package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
  110. package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
  111. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
  112. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
  113. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
  114. package/skills/yad-report/SKILL.md +1 -1
  115. package/skills/yad-review-companion/SKILL.md +3 -3
  116. package/skills/yad-review-gate/SKILL.md +22 -23
  117. package/skills/yad-review-gate/references/gating.md +13 -13
  118. package/skills/yad-run/SKILL.md +2 -2
  119. package/skills/yad-run/references/run-loop.md +2 -2
  120. package/skills/yad-spec/SKILL.md +2 -1
  121. package/skills/yad-spec/references/spec-handoff.md +19 -10
  122. package/skills/yad-stories/SKILL.md +2 -2
  123. package/skills/yad-sync-repos/SKILL.md +19 -3
  124. package/skills/yad-test-cases/SKILL.md +2 -2
  125. package/skills/yad-ui/SKILL.md +2 -2
  126. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
  127. package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
  128. package/skills/yad-checks/templates/hooks/yad-capture.sh +0 -53
@@ -0,0 +1,117 @@
1
+ // ledger-guard HARNESS HOOK — the local half of the CI gate of the same name (#171).
2
+ //
3
+ // The gate ledger is CI-owned in verified mode: `checks/ledger-guard.sh` rejects any non-bot commit
4
+ // that changes `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or `epics/*/reviews/*.md`,
5
+ // and the same files for the Foundation in `foundation/.sdlc/` and `foundation/reviews/` (E75).
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. The contract it passes through:
11
+ //
12
+ // stdin the harness's tool-call payload as JSON (optional)
13
+ // exit 0 allow
14
+ // exit 2 deny, reason on stderr
15
+ //
16
+ // A NODE SCRIPT, NOT A SHELL SCRIPT (E113), so it runs the same on Windows, macOS and Linux. Node is
17
+ // the one thing yadflow already needs. It imports nothing outside Node itself, because it runs from the
18
+ // Product, where yadflow may not be installed at all.
19
+ //
20
+ // Wired by `yad check --fix` for every harness that can refuse a write BEFORE it lands:
21
+ // Claude Code a `PreToolUse` hook in `.claude/settings.json`, pointing straight at THIS script
22
+ // Cursor a `preToolUse` hook in `.cursor/hooks.json`, pointing at `ledger-guard-cursor.mjs`
23
+ //
24
+ // DO NOT WIRE THIS SCRIPT INTO CURSOR DIRECTLY, and check the same thing before wiring it into any
25
+ // other harness by hand. Cursor's `preToolUse` is a PERMISSION hook: it wants a JSON verdict on
26
+ // stdout, and treats an empty or off-schema answer as a refusal. This script prints NOTHING when it
27
+ // allows — so wired there directly it would block every file write in the project, which is the
28
+ // opposite of the fail-open stance below. `ledger-guard-cursor.mjs` exists to speak that protocol.
29
+ //
30
+ // A harness that reads the EXIT CODE can use this script as it stands: `node <product>/hooks/ledger-guard.mjs`.
31
+ //
32
+ // FAIL-OPEN: if no `yad` can be found, this ALLOWS and says why on stderr. A guardrail that blocked
33
+ // every edit the moment an install went sideways would be worse than the problem. The CI gate fails
34
+ // CLOSED and is what actually protects the ledger.
35
+ import { spawnSync } from 'node:child_process';
36
+ import fs from 'node:fs';
37
+ import path from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
39
+
40
+ // ---- yad-resolve (the same block in every hooks/*.mjs; a test keeps the copies identical) ----------
41
+ // The Product root is this script's grandparent — `<product>/hooks/<script>.mjs` — found from the
42
+ // script's OWN location, never from the harness's working directory or its project-root variable
43
+ // (whose form on Windows, `C:\…` or `/c/…`, no harness documents). `realpathSync` so a script reached
44
+ // through a symbolic link still finds its own Product.
45
+ const PRODUCT_ROOT = path.dirname(path.dirname(fs.realpathSync(fileURLToPath(import.meta.url))));
46
+ const IS_WINDOWS = process.platform === 'win32';
47
+
48
+ // A command found on PATH, or null. On Windows the npm launchers are `.cmd` files.
49
+ function onPath(name) {
50
+ const exts = IS_WINDOWS ? ['.cmd', '.exe', ''] : [''];
51
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
52
+ if (!dir) continue;
53
+ for (const ext of exts) {
54
+ const full = path.join(dir, name + ext);
55
+ try { if (fs.statSync(full).isFile()) return full; } catch { /* not here */ }
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // Windows cannot start a `.cmd` launcher without a shell (Node refuses since 18.20.2), so those run
62
+ // through one, each word in double quotes. A Windows path cannot hold a `"`, so the quoting is whole.
63
+ const quoteForCmd = (word) => `"${String(word).replace(/"/g, '')}"`;
64
+
65
+ // How to run `yad`, cheapest and most specific first: an explicit override, then the copy installed in
66
+ // this Product, then `yad` on PATH, then a network-free npx. `--no-install` matters — a hook runs on
67
+ // every tool call and must never pause an agent to download a package. Returns
68
+ // `{ file, args, shell }` or null.
69
+ function resolveYad() {
70
+ // YAD_BIN is commonly an interpreter plus a script ("node /path/to/yad.mjs"). Split on spaces, with a
71
+ // double-quoted word kept whole — Node on Windows usually lives under `C:\Program Files\`.
72
+ const override = [...(process.env.YAD_BIN || '').matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2]).filter(Boolean);
73
+ if (override.length) {
74
+ const [file, ...args] = override;
75
+ return { file, args, shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(file) };
76
+ }
77
+ const local = path.join(PRODUCT_ROOT, 'node_modules', 'yadflow', 'bin', 'yad.mjs');
78
+ if (fs.existsSync(local)) return { file: process.execPath, args: [local], shell: false };
79
+ const yad = onPath('yad');
80
+ if (yad) {
81
+ if (!IS_WINDOWS || !/\.(cmd|bat)$/i.test(yad)) return { file: yad, args: [], shell: false };
82
+ // An npm global install puts the package beside its launcher: run the script with this Node
83
+ // rather than through a shell whenever it is there.
84
+ const script = path.join(path.dirname(yad), 'node_modules', 'yadflow', 'bin', 'yad.mjs');
85
+ if (fs.existsSync(script)) return { file: process.execPath, args: [script], shell: false };
86
+ return { file: yad, args: [], shell: true };
87
+ }
88
+ const npx = onPath('npx');
89
+ if (npx) return { file: npx, args: ['--no-install', 'yadflow'], shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(npx) };
90
+ return null;
91
+ }
92
+
93
+ // Run the resolved `yad` with `args` appended. `options` go to spawnSync.
94
+ function runYad(yad, args, options) {
95
+ const all = [...yad.args, ...args];
96
+ if (yad.shell) return spawnSync([yad.file, ...all].map(quoteForCmd).join(' '), { ...options, shell: true, windowsHide: true });
97
+ return spawnSync(yad.file, all, { ...options, windowsHide: true });
98
+ }
99
+ // ---- end yad-resolve ---------------------------------------------------------------------------------
100
+
101
+ const yad = resolveYad();
102
+ if (!yad) {
103
+ process.stderr.write(` • yad hook: no \`yad\` on PATH and none installed in ${PRODUCT_ROOT} — allowing (install yadflow to re-arm the ledger guard)\n`);
104
+ process.exit(0);
105
+ }
106
+
107
+ // Run it rather than hand the process over, so the exit code can be mapped. ONLY an explicit deny (2)
108
+ // blocks: a `yad` that is present but cannot run — an `npx --no-install` with no yadflow to find, a
109
+ // crash, a broken install — must not read as a refusal. Fail-open is the whole stance of this hook;
110
+ // the CI gate is what fails closed. stdin, stdout and stderr are all the harness's own.
111
+ const r = runYad(yad, ['hook', 'ledger-guard', ...process.argv.slice(2)], { stdio: 'inherit' });
112
+ if (r.status === 2) process.exit(2);
113
+ if (r.status !== 0) {
114
+ const why = r.error ? r.error.message : `exited ${r.status ?? r.signal}`;
115
+ process.stderr.write(` • yad hook: \`yad hook ledger-guard\` ${why} — allowing (run \`yad doctor\` to check the install)\n`);
116
+ }
117
+ process.exit(0);
@@ -0,0 +1,117 @@
1
+ // yad-capture HARNESS HOOK — background capture of Shape artifacts (E43).
2
+ //
3
+ // Runs AFTER an agent edits a file, and snapshots every changed artifact of the Product onto the
4
+ // person's private `yad/wip/<name>/<epic>` branches — built with git plumbing, so the checkout, the
5
+ // index and the current branch are never touched. The push runs in the background, at most once
6
+ // every few minutes. The decision lives in `yad capture --hook`; this file only locates `yad`.
7
+ //
8
+ // Wired by `yad check --fix`, in BOTH ledger modes:
9
+ // Claude Code a `PostToolUse` hook in `.claude/settings.json`: node "$CLAUDE_PROJECT_DIR/hooks/yad-capture.mjs" --format claude
10
+ // Cursor an `afterFileEdit` hook in `.cursor/hooks.json`: node hooks/yad-capture.mjs
11
+ // Any other harness: run `node <product>/hooks/yad-capture.mjs` (or `yad capture --hook`) after a file write.
12
+ //
13
+ // A NODE SCRIPT, NOT A SHELL SCRIPT (E113), so it runs the same on Windows, macOS and Linux.
14
+ //
15
+ // The contract: it ALWAYS exits 0 — a capture must never block or slow an agent. A problem is one line on
16
+ // stderr. Stdout carries ONE thing, and only with `--format claude`: when a file this edit changed is also
17
+ // being edited by someone else (E46 claims), a PostToolUse JSON note the agent reads. Otherwise it prints
18
+ // nothing. The format is passed by the entry and never guessed from the environment: Cursor sets
19
+ // `CLAUDE_PROJECT_DIR` too, as an alias. `YAD_CAPTURE=0`, or `"capture": false` in the Product config,
20
+ // turns it off.
21
+ import { spawnSync } from 'node:child_process';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ // ---- yad-resolve (the same block in every hooks/*.mjs; a test keeps the copies identical) ----------
27
+ // The Product root is this script's grandparent — `<product>/hooks/<script>.mjs` — found from the
28
+ // script's OWN location, never from the harness's working directory or its project-root variable
29
+ // (whose form on Windows, `C:\…` or `/c/…`, no harness documents). `realpathSync` so a script reached
30
+ // through a symbolic link still finds its own Product.
31
+ const PRODUCT_ROOT = path.dirname(path.dirname(fs.realpathSync(fileURLToPath(import.meta.url))));
32
+ const IS_WINDOWS = process.platform === 'win32';
33
+
34
+ // A command found on PATH, or null. On Windows the npm launchers are `.cmd` files.
35
+ function onPath(name) {
36
+ const exts = IS_WINDOWS ? ['.cmd', '.exe', ''] : [''];
37
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
38
+ if (!dir) continue;
39
+ for (const ext of exts) {
40
+ const full = path.join(dir, name + ext);
41
+ try { if (fs.statSync(full).isFile()) return full; } catch { /* not here */ }
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Windows cannot start a `.cmd` launcher without a shell (Node refuses since 18.20.2), so those run
48
+ // through one, each word in double quotes. A Windows path cannot hold a `"`, so the quoting is whole.
49
+ const quoteForCmd = (word) => `"${String(word).replace(/"/g, '')}"`;
50
+
51
+ // How to run `yad`, cheapest and most specific first: an explicit override, then the copy installed in
52
+ // this Product, then `yad` on PATH, then a network-free npx. `--no-install` matters — a hook runs on
53
+ // every tool call and must never pause an agent to download a package. Returns
54
+ // `{ file, args, shell }` or null.
55
+ function resolveYad() {
56
+ // YAD_BIN is commonly an interpreter plus a script ("node /path/to/yad.mjs"). Split on spaces, with a
57
+ // double-quoted word kept whole — Node on Windows usually lives under `C:\Program Files\`.
58
+ const override = [...(process.env.YAD_BIN || '').matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2]).filter(Boolean);
59
+ if (override.length) {
60
+ const [file, ...args] = override;
61
+ return { file, args, shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(file) };
62
+ }
63
+ const local = path.join(PRODUCT_ROOT, 'node_modules', 'yadflow', 'bin', 'yad.mjs');
64
+ if (fs.existsSync(local)) return { file: process.execPath, args: [local], shell: false };
65
+ const yad = onPath('yad');
66
+ if (yad) {
67
+ if (!IS_WINDOWS || !/\.(cmd|bat)$/i.test(yad)) return { file: yad, args: [], shell: false };
68
+ // An npm global install puts the package beside its launcher: run the script with this Node
69
+ // rather than through a shell whenever it is there.
70
+ const script = path.join(path.dirname(yad), 'node_modules', 'yadflow', 'bin', 'yad.mjs');
71
+ if (fs.existsSync(script)) return { file: process.execPath, args: [script], shell: false };
72
+ return { file: yad, args: [], shell: true };
73
+ }
74
+ const npx = onPath('npx');
75
+ if (npx) return { file: npx, args: ['--no-install', 'yadflow'], shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(npx) };
76
+ return null;
77
+ }
78
+
79
+ // Run the resolved `yad` with `args` appended. `options` go to spawnSync.
80
+ function runYad(yad, args, options) {
81
+ const all = [...yad.args, ...args];
82
+ if (yad.shell) return spawnSync([yad.file, ...all].map(quoteForCmd).join(' '), { ...options, shell: true, windowsHide: true });
83
+ return spawnSync(yad.file, all, { ...options, windowsHide: true });
84
+ }
85
+ // ---- end yad-resolve ---------------------------------------------------------------------------------
86
+
87
+ // Drain the payload the harness sends on stdin, so it never sees a broken pipe — but never wait on it: not
88
+ // at all from a terminal (someone ran this by hand), and at most a second on a pipe that never closes.
89
+ function drainStdin() {
90
+ if (process.stdin.isTTY) return Promise.resolve();
91
+ return new Promise((resolve) => {
92
+ const done = () => { clearTimeout(timer); process.stdin.pause(); resolve(); };
93
+ const timer = setTimeout(done, 1000);
94
+ process.stdin.on('data', () => {});
95
+ process.stdin.on('end', done);
96
+ process.stdin.on('error', done);
97
+ process.stdin.resume();
98
+ });
99
+ }
100
+
101
+ async function main() {
102
+ await drainStdin();
103
+ if (process.env.YAD_CAPTURE === '0') return;
104
+ const yad = resolveYad();
105
+ if (!yad) {
106
+ process.stderr.write(` • yad capture: no \`yad\` on PATH and none installed in ${PRODUCT_ROOT} — nothing captured\n`);
107
+ return;
108
+ }
109
+ // Only `--format claude` is passed through; anything else on the command line is not ours to forward.
110
+ const argv = process.argv.slice(2);
111
+ const i = argv.indexOf('--format');
112
+ const format = i >= 0 && argv[i + 1] === 'claude' ? ['--format', 'claude'] : [];
113
+ // stdin is closed for the child: the payload was drained above and capture reads none.
114
+ runYad(yad, ['capture', '--hook', ...format, '--dir', PRODUCT_ROOT], { stdio: ['ignore', 'inherit', 'inherit'] });
115
+ }
116
+
117
+ main().catch((e) => { process.stderr.write(` • yad capture: ${e?.message || e}\n`); }).finally(() => process.exit(0));
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: yad-commit
3
- description: 'Build helper of the gated SDLC. Commit ONE staged atomic change by the conventions — a Conventional-Commits subject, the fixed trailer order (Task → Contract-Change, plus an OPTIONAL Co-Authored-By), and an atomic-file guard (≤3 files). By default the commit carries NO AI footer: the human git author owns it, and a Co-Authored-By trailer is added ONLY when --ai <id> is explicitly passed (claude|copilot|cursor|coderabbit; default none = human-only). The flag is the sole switch — never add the footer on the AI''s own initiative. Drives the zero-dependency `yad commit` CLI; never auto-advances. Use when the user says "commit this", "commit by convention", or "make an atomic commit".'
3
+ description: 'Build helper of the gated SDLC. Commit ONE staged atomic change by the conventions — a Conventional-Commits subject, the fixed trailer order (Task → Contract-Change → Ledger-Override, plus an OPTIONAL Co-Authored-By), and an atomic-file guard (≤3 files). By default the commit carries NO AI footer: the human git author owns it, and a Co-Authored-By trailer is added ONLY when --ai <id> is explicitly passed (claude|copilot|cursor|coderabbit; default none = human-only). The flag is the sole switch — never add the footer on the AI''s own initiative. Drives the zero-dependency `yad commit` CLI; never auto-advances. Use when the user says "commit this", "commit by convention", or "make an atomic commit".'
4
4
  ---
5
5
 
6
6
  # SDLC — Commit by Convention (Build helper)
7
7
 
8
8
  **Goal:** Turn ONE staged atomic change into a single commit that satisfies the project conventions
9
9
  (`CONTRIBUTING.md` / `config.yaml` `build`): a Conventional-Commits subject, the fixed trailer order
10
- `Task → Contract-Change → Co-Authored-By` (the footer **off by default** — added only via an explicit
11
- `--ai`), and the atomic-file guard. This is the standalone commit step — the same engine `yad-implement`
10
+ `Task → Contract-Change → Ledger-Override → Co-Authored-By` (the override only with `--manual`; the footer
11
+ **off by default** — added only via an explicit `--ai`), and the atomic-file guard. This is the standalone commit step — the same engine `yad-implement`
12
12
  and `yad-ship` use. It **never auto-advances**; it just commits.
13
13
 
14
14
  ## Conventions
@@ -20,8 +20,9 @@ and `yad-ship` use. It **never auto-advances**; it just commits.
20
20
  - **Subject** — `<type>: <lowercase imperative description, no trailing period>`; types are
21
21
  `feat|fix|docs|refactor|test|perf|build|ci|chore|revert`; proper nouns/acronyms keep their case.
22
22
  - **Task trailer** — required on a code repo (anchors the `spec-link` + `commit-message` gates). Given
23
- with `--task` (a `<story>-T<NN>` id — an explicit value is validated against the `spec-link`
24
- contract, so a malformed id like a bare `EP-x-S01` fails locally instead of after a push), else
23
+ with `--task` (a `<story>-T<NN>` id, the story an `EP-<slug>-S<n>` with a lowercase slug — an explicit
24
+ value is validated against the `spec-link` contract, so a malformed id like a bare `EP-x-S01` or an
25
+ `EP-Demo-S01-T01` fails locally instead of after a push), else
25
26
  derived from the branch (`feat/<story>-<task>-…`). Product commits are not
26
27
  task-scoped, so the trailer is optional there — a missing-Task warning is informational on the Product
27
28
  (`spec-link` is a code-repo gate) and only flags a real gate failure in a code repo.
@@ -40,6 +41,12 @@ and `yad-ship` use. It **never auto-advances**; it just commits.
40
41
  the `Co-Authored-By` trailer appears only when this flag names a tool).
41
42
  - `task` — Task trailer (optional; derived from the branch when omitted).
42
43
  - `contractChange` — flag; mark the contract surface touched.
44
+ - `manual` + `reason` — the ledger override (E49), for a PERSON only. On a verified Product a local
45
+ pre-commit hook refuses a commit that changes a CI-owned file (the gate ledger, the Product index).
46
+ `--manual --reason "<why>"` commits past that hook and records the reason as a `Ledger-Override:`
47
+ trailer. It is refused when nothing staged is a file that hook refuses. The `ledger-guard` check on
48
+ the pull request still fails the commit, and quotes the reason. A reason that starts with `-` is
49
+ passed as `--reason=-…`.
43
50
 
44
51
  ## On Activation
45
52
 
@@ -50,7 +57,7 @@ staged, split it into separate commits rather than passing `--force`.
50
57
  ### Step 2 — Commit by convention
51
58
  Run the CLI from the repo root:
52
59
  ```
53
- yad commit --type <type> -m "<subject>" [--ai <id>] [--task <id>] [--contract-change] [--dry-run]
60
+ yad commit --type <type> -m "<subject>" [--ai <id>] [--task <id>] [--contract-change] [--manual --reason "<why>"] [--dry-run]
54
61
  ```
55
62
  Use `--dry-run` first to preview the exact message (subject + trailer block) without committing. The
56
63
  CLI validates the type, rejects a trailing period, and emits the trailers in the fixed order.
@@ -65,8 +72,12 @@ the architecture gate. To also open the PR/MR in the same step, use `yad-ship`.
65
72
  - **No AI footer by default.** A `Co-Authored-By` trailer is written ONLY when `--ai <id>` is explicitly
66
73
  passed; the default is a clean human-only commit. Never add it on your own initiative.
67
74
  - **The human author owns the commit.** The AI is at most a `Co-Authored-By` footer, never the author.
68
- - **Trailer order is fixed:** `Task → Contract-Change → Co-Authored-By` (the footer only when `--ai` is given).
75
+ - **Trailer order is fixed:** `Task → Contract-Change → Ledger-Override → Co-Authored-By` (the override only
76
+ with `--manual`, the footer only when `--ai` is given).
69
77
  - **Never widen the contract here.** A contract touch is flagged (`--contract-change`), not hidden.
78
+ - **Never pass `--manual` on your own initiative.** Committing past the ledger hook is a person's decision.
79
+ Pass it only when the user asked for it, with the reason they gave. `--manual` needs `--reason`, and the
80
+ reason must be one line.
70
81
 
71
82
  ## Reference
72
83
  - Branch/commit conventions + the file-boundary rule: `../yad-implement/references/implement-conventions.md`.
@@ -26,7 +26,7 @@ markdown-only exactly as before.
26
26
  shape as Impeccable's slash-commands, not Repomix's `npx`. The skill detects the MCP and degrades when
27
27
  it is absent; it never installs an MCP server.
28
28
  - Registry: `{project-root}/.sdlc/design.json` (project-wide, shared across all epics — NOT per-epic),
29
- the sibling of `.sdlc/repos.json` and `.sdlc/hub.json`.
29
+ the sibling of `.sdlc/repos.json` and `.sdlc/product.json`.
30
30
  - Per-epic screen→frame links are written later by `yad-ui` (`epics/EP-<slug>/.sdlc/design-links.json`),
31
31
  not here.
32
32
  - Speak in the `communication_language` set in `{project-root}/.sdlc/config.yaml`; write documents in `document_output_language`.
@@ -47,7 +47,7 @@ root, not under any `epics/EP-<slug>/.sdlc/`.
47
47
  ## Git tracking
48
48
 
49
49
  Commit the **registry** (`design.json`) — it is small, reviewable, and holds no secrets (references
50
- only). This mirrors how `repos.json` and `hub.json` are committed.
50
+ only). This mirrors how `repos.json` and `product.json` are committed.
51
51
 
52
52
  ## Greenfield
53
53
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-connect-docs
3
- description: 'Connects a docs/Pages publishing target to the Product so the interactive-docs steps can build and deploy the generated SPA — not just commit its source. Registers the target into the project-wide .sdlc/docs.json (GitHub Pages / GitLab Pages / build-only), auto-detecting the platform from .sdlc/hub.json and resolving the Vite base path, with local-user auth and no stored tokens. Detects whether gh/glab is present and degrades to build-only when absent. Run at setup or any time the publish target changes. Reusable, idempotent, refreshable. Use when the user says "connect docs", "connect Pages", "refresh the docs connection", or "list the docs connection".'
3
+ description: 'Connects a docs/Pages publishing target to the Product so the interactive-docs steps can build and deploy the generated SPA — not just commit its source. Registers the target into the project-wide .sdlc/docs.json (GitHub Pages / GitLab Pages / build-only), auto-detecting the platform from .sdlc/product.json and resolving the Vite base path, with local-user auth and no stored tokens. Detects whether gh/glab is present and degrades to build-only when absent. Run at setup or any time the publish target changes. Reusable, idempotent, refreshable. Use when the user says "connect docs", "connect Pages", "refresh the docs connection", or "list the docs connection".'
4
4
  ---
5
5
 
6
6
  # SDLC — Connect a Docs/Pages Target (make the docs steps publishable)
@@ -26,7 +26,7 @@ at a local `dist/` — build-only, no publish, exactly as before.
26
26
  never installed by this skill, never given a token. Absent ⇒ degrade to build-only (`source:
27
27
  "unavailable"`).
28
28
  - Registry: `{project-root}/.sdlc/docs.json` (project-wide, shared across all epics + the overview —
29
- NOT per-epic), the sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`.
29
+ NOT per-epic), the sibling of `.sdlc/product.json`, `.sdlc/repos.json`, and `.sdlc/design.json`.
30
30
  - Per-epic / overview build manifests (`docs-build.json`) are written later by `yad-docs` /
31
31
  `yad-docs-overview`, not here. This skill describes the *connection*; it does not build.
32
32
  - Speak in the `communication_language` set in `{project-root}/.sdlc/config.yaml`; write documents in `document_output_language`.
@@ -34,17 +34,21 @@ at a local `dist/` — build-only, no publish, exactly as before.
34
34
  ## Inputs
35
35
 
36
36
  - `action` — `connect` (default) | `refresh` | `list` | `disconnect`.
37
- - `target` — `github-pages` | `gitlab-pages` | `none`. Default **auto-detected** from `.sdlc/hub.json`
37
+ - `target` — `github-pages` | `gitlab-pages` | `none`. Default **auto-detected** from `.sdlc/product.json`
38
38
  `platform` (github → `github-pages`, gitlab → `gitlab-pages`, null/no Product → `none`).
39
- - `scope` — `hub` (default) | `<repo-name>` | `dedicated`. Where the Pages site is published from (the
40
- Product repo, one connected code repo, or a dedicated docs repo).
39
+ - `scope` — `product` (default) | `<repo-name>` | `dedicated`. Where the Pages site is published from (the
40
+ Product repo, one connected code repo, or a dedicated docs repo). `hub` is the old name for `product`:
41
+ a `docs.json` written before 4.0 says `"scope": "hub"`, and it means the same thing — read it as
42
+ `product`, and write `product` the next time you write the file. A connected code repo that is itself named
43
+ `product` is recorded by its registry `path` (for example `repos/product`), never its name, so it is not read
44
+ as the Product.
41
45
  - `public` — `true` (default) | `false`. Whether the published site is public.
42
46
  - `base_path` — optional explicit override of the Vite `base` (otherwise resolved, Step 2).
43
47
 
44
48
  ## On Activation
45
49
 
46
50
  ### Step 1 — Resolve the target + detect the platform (the publish adapter)
47
- Determine the `target`. If not given, read `{project-root}/.sdlc/hub.json` `platform` and map it the same
51
+ Determine the `target`. If not given, read `{project-root}/.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name) `platform` and map it the same
48
52
  way the Product bridge maps repos: `github` → `github-pages`, `gitlab` → `gitlab-pages`, `null`/no Product →
49
53
  `none` (deliberate build-only). Reject a `target` value outside the three providers (fall back to the
50
54
  detected default with a warning, the way `registerRepo` falls back on an unknown platform).
@@ -61,8 +65,9 @@ tokens**; everything in the registry is a plain reference. Do **not** install a
61
65
 
62
66
  ### Step 2 — Decide the publish scope + resolve the base path
63
67
  Resolve `scope` → `publishRepo`:
64
- - `hub` (default) → publish from the Product repo (read its name from `hub.json` `git_url`).
65
- - `<repo-name>` → publish from that connected code repo (must exist in `.sdlc/repos.json`).
68
+ - `product` (default; `hub` in an older `docs.json`) → publish from the Product repo (read its name from `product.json` `git_url`).
69
+ - `<repo-name>` → publish from that connected code repo (must exist in `.sdlc/repos.json`). A value with a `/` in
70
+ it is that repo's registry `path` (the form a repo named `product` is recorded in).
66
71
  - `dedicated` → a dedicated docs repo the user names (recorded as `publishRepo`).
67
72
 
68
73
  Resolve `basePath` (the Vite `base`) per the table in `references/docs-registry.md`:
@@ -78,8 +83,8 @@ Upsert into `{project-root}/.sdlc/docs.json` (create the file + parent `.sdlc/`
78
83
  ```json
79
84
  {
80
85
  "target": "github-pages",
81
- "scope": "hub",
82
- "publishRepo": "<repo or hub name>",
86
+ "scope": "product",
87
+ "publishRepo": "<repo or Product name>",
83
88
  "basePath": "/<repo>/",
84
89
  "public": true,
85
90
  "auth": "user",
@@ -89,7 +94,7 @@ Upsert into `{project-root}/.sdlc/docs.json` (create the file + parent `.sdlc/`
89
94
  }
90
95
  ```
91
96
 
92
- - `target: "none"` records a deliberate build-only project: `{ "target": "none", "scope": "hub",
97
+ - `target: "none"` records a deliberate build-only project: `{ "target": "none", "scope": "product",
93
98
  "publishRepo": null, "basePath": "/", "source": "unavailable", ... }`.
94
99
  - `connect` is **idempotent** — re-running it overwrites the single connection in place (a project has
95
100
  one docs target at a time; switching targets is just another `connect`).
@@ -1,7 +1,7 @@
1
1
  # `.sdlc/docs.json` — the docs/Pages registry
2
2
 
3
3
  Project-wide, shared across every epic's docs site **and** the project overview site (NOT per-epic).
4
- The sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`. Written by
4
+ The sibling of `.sdlc/product.json`, `.sdlc/repos.json`, and `.sdlc/design.json`. Written by
5
5
  `yad-connect-docs`; read by `yad-docs`, `yad-docs-overview`, `yad-docs-sync`, and the `yad docs` CLI.
6
6
  Holds **no credentials** — every field is a plain reference. Auth is always the local user's own
7
7
  `gh`/`glab`/git session.
@@ -11,8 +11,8 @@ Holds **no credentials** — every field is a plain reference. Auth is always th
11
11
  ```json
12
12
  {
13
13
  "target": "github-pages | gitlab-pages | none",
14
- "scope": "hub | <repo-name> | dedicated",
15
- "publishRepo": "<repo or hub name>",
14
+ "scope": "product | <repo-name> | dedicated",
15
+ "publishRepo": "<repo or Product name>",
16
16
  "basePath": "/<repo>/",
17
17
  "public": true,
18
18
  "auth": "user",
@@ -25,7 +25,7 @@ Holds **no credentials** — every field is a plain reference. Auth is always th
25
25
  | Field | Meaning |
26
26
  |-------|---------|
27
27
  | `target` | The publish adapter. `none` = deliberate build-only (no publish, no error). |
28
- | `scope` | Where the Pages site publishes from: the `hub` repo, one connected `<repo-name>`, or a `dedicated` docs repo. |
28
+ | `scope` | Where the Pages site publishes from: the `product` repo, one connected `<repo-name>`, or a `dedicated` docs repo. A file written before 4.0 says `hub`, the old name for `product`; read it the same way. A code repo that is itself named `product` is recorded by its registry `path` (for example `repos/product`), so it is not read as the Product. |
29
29
  | `publishRepo` | The concrete repo name resolved from `scope`. `null` when `target: "none"`. |
30
30
  | `basePath` | The Vite `base` substituted into each generated site (resolution table below). Normalized to a leading + trailing `/`. |
31
31
  | `public` | Whether the published site is public. |
@@ -36,15 +36,15 @@ Holds **no credentials** — every field is a plain reference. Auth is always th
36
36
  `target: "none"` records `{ "target": "none", "publishRepo": null, "basePath": "/", "source":
37
37
  "unavailable", ... }`.
38
38
 
39
- ## Platform auto-detection (from `.sdlc/hub.json`)
39
+ ## Platform auto-detection (from `.sdlc/product.json`)
40
40
 
41
- When `target` is not given, map the Product's `platform` the same way `yad-connect-repos` maps a repo host:
41
+ When `target` is not given, read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name) and map the Product's `platform` the same way `yad-connect-repos` maps a repo host:
42
42
 
43
- | `hub.json` `platform` | default `target` |
43
+ | `product.json` `platform` | default `target` |
44
44
  |-----------------------|------------------|
45
45
  | `github` | `github-pages` |
46
46
  | `gitlab` | `gitlab-pages` |
47
- | `null` / no hub.json | `none` (build-only) |
47
+ | `null` / no settings file | `none` (build-only) |
48
48
 
49
49
  ## Base-path resolution table
50
50
 
@@ -27,7 +27,7 @@ explains the concept. The learning layer is **purely opt-in and never blocks a g
27
27
  an MCP like the design/testing tools (DeepTutor ships no MCP server). The skill detects the **binary on
28
28
  PATH** and degrades when it is absent; it never installs DeepTutor.
29
29
  - Registry: `{project-root}/.sdlc/learning.json` (project-wide, shared across all epics — NOT per-epic),
30
- the sibling of `.sdlc/repos.json`, `.sdlc/hub.json`, `.sdlc/design.json`, and `.sdlc/testing.json`.
30
+ the sibling of `.sdlc/repos.json`, `.sdlc/product.json`, `.sdlc/design.json`, and `.sdlc/testing.json`.
31
31
  - Per-epic, per-member learning records + rendered tutorials are written later by `yad-learn`
32
32
  (`epics/EP-<slug>/.sdlc/learning-records.json` and `epics/EP-<slug>/learning/`), not here.
33
33
  - Speak in the `communication_language` set in `{project-root}/.sdlc/config.yaml`; write documents in `document_output_language`.
@@ -51,7 +51,7 @@ root, not under any `epics/EP-<slug>/.sdlc/`.
51
51
  ## Git tracking
52
52
 
53
53
  Commit the **registry** (`learning.json`) — it is small, reviewable, and holds no secrets (references
54
- only). This mirrors how `repos.json`, `hub.json`, `design.json`, and `testing.json` are committed.
54
+ only). This mirrors how `repos.json`, `product.json`, `design.json`, and `testing.json` are committed.
55
55
 
56
56
  ## Greenfield
57
57
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-connect-repos
3
- description: 'Connects code repos to the Product so the front/"brain" phases are code-aware. Registers N code repos (GitHub or GitLab, local-user auth, no stored tokens) into the project-wide .sdlc/repos.json, then caches an AI-readable picture of each — a compressed Repomix pack and a lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Run at one-time setup or any time a new repo is added. Reusable, idempotent, refreshable; staleness is tracked by HEAD sha. `yad repo refresh --push` publishes the refreshed code-maps + registry to the Product default branch as a chore(hub): sync code-context [skip ci] audit commit. Also drafts each code repo's risk map (.sdlc/risk-map — a level per directory, no names), classified by reading the code and marked guessed for a person to confirm. Use when the user says "connect a repo", "connect the code repos", "refresh the code context", "list connected repos", or "push the code-map refresh".'
3
+ description: 'Connects code repos to the Product so the front/"brain" phases are code-aware. Registers N code repos (GitHub or GitLab, local-user auth, no stored tokens) into the project-wide .sdlc/repos.json, then caches an AI-readable picture of each — a compressed Repomix pack and a lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Run at one-time setup or any time a new repo is added. Reusable, idempotent, refreshable; staleness is tracked by HEAD sha. `yad repo refresh --push` publishes the refreshed code-maps + registry to the Product default branch as a chore(product): sync code-context [skip ci] audit commit. Also drafts each code repo''s risk map (.sdlc/risk-map — a level per directory, no names), classified by reading the code and marked guessed for a person to confirm. Use when the user says "connect a repo", "connect the code repos", "refresh the code context", "list connected repos", or "push the code-map refresh".'
4
4
  ---
5
5
 
6
6
  # SDLC — Connect Code Repos (make the brain code-aware)
@@ -28,7 +28,7 @@ repo itself — a draft of that repo's risk map (Step 3b), which the team commit
28
28
 
29
29
  ## Inputs
30
30
 
31
- - `action` — `connect` | `refresh` | `list` | `disconnect` | `detect-hub` (default `connect`).
31
+ - `action` — `connect` | `refresh` | `list` | `disconnect` | `detect-product` (default `connect`; `detect-hub`, the old name, still works).
32
32
  - `repo` — the repo's short name (the key used in stories' `repos:` tag, e.g. `backend`).
33
33
  - `path` — local path to the code repo (relative to `{project-root}` or absolute). For local repos.
34
34
  It must resolve inside the **workspace** — the project root's parent — so the standard layout, where
@@ -50,6 +50,9 @@ repo itself — a draft of that repo's risk map (Step 3b), which the team commit
50
50
  directly in `$HOME` — with the Product at `~/product` the workspace becomes `~` and every home-dir
51
51
  sibling turns into a registrable repo. The workspace directory itself (`..`) is never registrable.
52
52
  - `git_url` — optional remote (SSH or HTTPS; GitHub or GitLab). Used when the repo is not yet on disk.
53
+ `yad join` and `yad repo clone` clone only a network address (`https://`, `http://`, `ssh://`,
54
+ `git://`, `user@host:path`, or an ssh alias `host:path`); a local path is refused unless
55
+ `YAD_ALLOW_LOCAL_REMOTES=1` is set, and then only a folder that exists outside the workspace.
53
56
 
54
57
  yadflow keeps **no list of people** (E62): no roster, no roles, no repo owners, no commit emails. Do not
55
58
  ask for them and do not write them.
@@ -59,7 +62,13 @@ ask for them and do not write them.
59
62
  ### Step 1 — Resolve the repo and its auth (GitHub + GitLab, local user)
60
63
  Determine where the code is:
61
64
  - If `path` is given and is a git repo (`.git` present) → use it in place.
62
- - Else if `git_url` is given → **clone it as the local user** into a working location
65
+ - A teammate joining an existing Product gets every registered repo cloned at its recorded `path` by
66
+ `yad join <url>` (E79) — no need to clone them one by one here.
67
+ - A repo that is **already registered** but missing on this machine (a teammate registered it after you
68
+ joined) → run **`yad repo clone [<name>]`** (E81). It clones at the recorded `path`, with the same
69
+ checks as `join`, and never touches a repo that is already there. `yad repo list` and `yad doctor`
70
+ name such a repo as not cloned. Clone by hand only a repo that is **not registered yet**.
71
+ - Else if `git_url` is given (a repo not registered yet) → **clone it as the local user** into a working location
63
72
  (`{code_repos_root}/<repo>/` by `config.yaml` `build.code_repos_root`, or a path the user names):
64
73
  ```
65
74
  git clone <git_url> <dest>
@@ -152,7 +161,7 @@ the Shape phases will now load this repo's code-map. Nothing auto-advances; this
152
161
  `syncedHead` + `lastSyncedAt`. Same machinery as `connect`. Once the AI has regenerated the
153
162
  `code-map.md` (Step 3), publish it to the Product with **`yad repo refresh <repo> --push`**: it
154
163
  commits the tracked code-maps + `.sdlc/repos.json` (never the gitignored `pack.md`) as one
155
- audit-trail commit `chore(hub): sync code-context — <repos> by @<login> [skip ci]` and pushes it
164
+ audit-trail commit `chore(product): sync code-context — <repos> by @<login> [skip ci]` and pushes it
156
165
  straight to the Product's **default branch** (add `--allow-branch` to commit on a non-default branch).
157
166
  This is the code-context analogue of `yad checkpoint` — human-owned machine state, no Task trailer,
158
167
  no Co-Authored-By.
@@ -164,10 +173,13 @@ the Shape phases will now load this repo's code-map. Nothing auto-advances; this
164
173
  ## Product detection (the Shape review bridge)
165
174
 
166
175
  The Product is itself a git repo on a platform. This action records that so the Shape review/comment/
167
- approval cycle can run through a real PR/MR on the Product (`yad-review-gate` + `yad-hub-bridge`). It
168
- writes only `{project-root}/.sdlc/hub.json` (`config.yaml` `product.config` (older projects: `hub.config`)) — never an epic's state/approvals.
176
+ approval cycle can run through a real PR/MR on the Product (`yad-review-gate` + `yad-product-bridge`). It
177
+ writes only `{project-root}/.sdlc/product.json` and its older name `.sdlc/hub.json` (`config.yaml` `product.config` (older projects: `hub.config`)) — never an epic's state/approvals.
169
178
 
170
- - **`detect-hub`** — detect the Product's own platform and upsert `.sdlc/hub.json`. Run
179
+ - **`detect-product`** — detect the Product's own platform and upsert the Product settings: read
180
+ `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name), then write
181
+ `.sdlc/product.json` and copy it byte for byte to `.sdlc/hub.json` (`cp .sdlc/product.json .sdlc/hub.json`;
182
+ both until v5 — two that differ by even a space are refused, YAD-STATE-008 — E122). Run
171
183
  `git remote get-url origin` **on the Product** and read the host with the SAME logic Step 1 uses for code
172
184
  repos: `github.com` → `github`, GitLab host → `gitlab`, no remote → `platform: null`. Record
173
185
  `git_url`, `default_branch`, `detectedAt`, and **all three of** `ledger`, `bridge_enabled` and
@@ -204,6 +216,17 @@ platform login that gave it. The platform, not a stored list, decides who can ap
204
216
  If the Product has no remote (`platform: null`) or the verified ledger is disabled, the Shape gate runs
205
217
  local with no error — the verified ledger is purely additive.
206
218
 
219
+ ## The Product-link record (E120)
220
+ Each connected code repo also carries `.sdlc/product-link.json` — the Product's `git_url`, the `path`
221
+ where CI checks the Product out (`.yad/product` by default) and its `default_branch`. It is not written
222
+ here: `yad setup`, `yad check --fix` and `yad update` write it (from the Product's own settings, with
223
+ any user name or password taken out of `git_url`) into every connected repo, and `--push` commits it
224
+ to each repo's default branch — a record on disk that is not committed as it stands is committed by the
225
+ next `--push`, though `yad check` reads it as `ok`. The Product-reading gates read it from
226
+ the base; `checks/product-checkout.sh` uses it with the `YAD_PRODUCT_TOKEN` secret. After connecting a
227
+ repo, run `yad check --fix --push`; `yad doctor` warns `repos:product-link-missing` until it is on
228
+ the repo's default branch.
229
+
207
230
  ## Live on-demand (the third context layer)
208
231
  The cached pack + map are the default. When a Shape phase needs an **area** not in the map, it may
209
232
  re-run Repomix **live**, scoped to that area:
@@ -229,7 +252,7 @@ it does not silently re-pack. Refreshing the cache is a human decision. Document
229
252
  ## Reference
230
253
  - Registry schema + freshness rule: `references/repos-registry.md`.
231
254
  - The risk map — format, rubric, what the agent may change: `references/risk-map.md`.
232
- - Product config (the review bridge): `references/hub-config.md`.
255
+ - Product config (the review bridge): `references/product-config.md`.
233
256
  - Repomix command, secret-scan, degrade path, the code-map prompt, and live on-demand:
234
257
  `references/code-context.md`.
235
258
  - The repomix discipline this reuses (one-feature-at-a-time variant): `../yad-backfill/references/backfill.md`.
@@ -81,7 +81,7 @@ side-effect:** when a repo is stale (HEAD ≠ `syncedHead`), the phase **flags i
81
81
  "`<repo>` is stale; run `yad repo refresh <repo>` to re-pack the cache + `syncedHead`" — rather than
82
82
  silently re-packing the whole repo. A phase never refreshes the registry on its own; the human runs
83
83
  `yad repo refresh` (or `yad check --fix`). After the AI regenerates the code-map, `yad repo refresh
84
- --push` publishes the refreshed code-maps + registry to the Product's default branch as a `chore(hub): sync
84
+ --push` publishes the refreshed code-maps + registry to the Product's default branch as a `chore(product): sync
85
85
  code-context … [skip ci]` audit commit (never the pack's content; `--allow-branch` overrides the branch
86
86
  guard). The `pack.md` is gitignored — `yad repo refresh`/`yad setup` scaffold
87
87
  `.sdlc/code-context/*/pack.md` into the Product `.gitignore` (so a regenerated pack never dirties the tree),