yadflow 4.0.0-next.3 → 4.0.0-next.4

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 +169 -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
package/bin/yad.mjs CHANGED
@@ -1,14 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  // `yad` — setup/maintenance + the PR-driven review gate + build helpers for the SDLC module.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import { VERSION } from '../cli/manifest.mjs';
4
6
  import { c, log, warn, closePrompts, askYesNo, refuse, beginJSON, inJSON, emitJSON, jsonEmitted, jsonFailure, stripAnsi, isPlainObject, ENVELOPE_KEYS } from '../cli/lib.mjs';
5
- import { runLedgerGuardHook } from '../cli/hook.mjs';
7
+ import { runLedgerGuardHook, runStagedLedgerGuard } from '../cli/hook.mjs';
6
8
 
7
9
  const helpText = (profiles) => `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
8
10
 
9
11
  ${c.dim('Every command but `yad hook` takes --json: one JSON object on stdout, every other line on stderr')}
10
12
  ${c.dim('(docs/CLI.md, "--json on every command").')}
11
13
 
14
+ ${c.bold('Start a workspace')} ${c.dim('(the folder holding product/ and the code repos side by side)')}
15
+ ${c.dim('--dir: for init, the workspace; for new and join, the folder the workspace is made in')}
16
+ yad new <name> Greenfield: make <name>/product/, git init it, run setup inside it. Creates
17
+ no remote — prints the gh/glab line to run yourself. Takes the setup flags
18
+ yad init Brownfield, in the folder that already holds your repos: make product/ (or
19
+ use the one there; --path <folder> names it), run setup, offer each repo found
20
+ yad join <url> [folder]
21
+ Clone the Product into [folder]/product/ (default: the URL's last name) and
22
+ every repo it registers; a failed repo clone is reported, never fatal. Then
23
+ only per-machine steps: git-ignored skill copies + the git pre-commit hook.
24
+ Never commits, pushes, or changes a shared file
25
+
12
26
  ${c.bold('Setup & maintenance')}
13
27
  yad setup Guided first-run setup (profile interview, install, connect & wire repos)
14
28
  profile flags: --solo | --team <n>, --greenfield | --brownfield,
@@ -29,11 +43,13 @@ ${c.bold('Setup & maintenance')}
29
43
  'yad check --fix --push'; --allow-branch permits a non-default branch
30
44
  yad doctor [--json] Environment + state health: tools/auth, config files,
31
45
  repo paths, epic ledgers (exit 1 on any failure)
32
- yad migrate [--apply] [--json] Move this project's state files onto the shape
46
+ yad migrate [--apply] [--keep product|hub] [--json] Move this project's state files onto the shape
33
47
  this yadflow expects. Prints what WOULD change and writes
34
48
  nothing until --apply, which copies each file it rewrites to
35
49
  <file>.yad-orig first. Safe to run twice — the second run
36
- reports there is nothing to do
50
+ reports there is nothing to do. When .sdlc/product.json and
51
+ .sdlc/hub.json (or an epic's product-prs.json and hub-prs.json)
52
+ disagree, --apply asks which copy to keep; --keep answers it
37
53
  yad sync-status [epic] Update artifact frontmatter status (draft/in-review/approved)
38
54
  from .sdlc/state.json — all epics if omitted (--dry-run to preview)
39
55
  yad report [-m <text>] File a bug in the yadflow repo with auto-scrubbed diagnostics
@@ -46,7 +62,10 @@ ${c.bold('Setup & maintenance')}
46
62
  --format cursor the verdict is JSON on stdout instead, for a harness
47
63
  whose pre-edit hook asks for permission rather than reading an exit
48
64
  code. Wired into .claude/settings.json and .cursor/hooks.json by
49
- setup / check --fix. YAD_HOOK_DISABLE=1 skips.
65
+ setup / check --fix. With --staged it judges the files a commit is
66
+ about to record instead — the git pre-commit hook check --fix
67
+ installs in this clone refuses a person's hand commit to the ledger.
68
+ YAD_HOOK_DISABLE=1 skips.
50
69
 
51
70
  ${c.bold('Team usage (EM adoption & behavior report)')}
52
71
  yad usage Build a per-member report (HTML) from git + the SDLC ledgers
@@ -170,17 +189,23 @@ ${c.bold('Review gate (Shape)')}
170
189
  --merged advances the step + flips artifact status on the default branch
171
190
 
172
191
  ${c.bold('Build helpers')}
173
- yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
174
- yad open-pr [--repo <name>] Open a task PR/MR against the repo's DEFAULT branch (never a
192
+ yad commit --type <t> -m <subject> [--manual --reason <why>]
193
+ Commit by convention (trailers, atomic guard); --manual is
194
+ the door past the Product's ledger hook, reason recorded
195
+ yad open-pr [--repo <name>] [--title <text>]
196
+ Open a task PR/MR against the repo's DEFAULT branch (never a
175
197
  hardcoded main; --base overrides) — stage-aware on the Product: a
176
198
  review/EP-* branch opens the Shape artifact-review PR
177
199
  (delegates to gate open), any other Product branch uses the
178
- code-task template
179
- yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
200
+ code-task template. --title sets the PR/MR title (default: -m,
201
+ else the last commit's subject)
202
+ yad ship --type <t> -m <subject> [--title <text>]
203
+ Commit AND open the task PR/MR in one step (stage-aware).
204
+ --title sets the PR/MR title (default: the commit subject)
180
205
  yad checkpoint [--push] Commit the machine-written Build state on the Product
181
206
  (trust-log/build-log/build-state) — plus any story
182
207
  status: flip (→ in-build/shipped) backed by a build-log
183
- ship — as one audit-trail chore(hub) commit; default
208
+ ship — as one audit-trail chore(product) commit; default
184
209
  branch only (--allow-branch to override); no-op when clean
185
210
  yad checkpoint --retro-ship <epic>/<story> --repo <r>
186
211
  Record a retroactive build-log ship for a PRE-TRACKING story
@@ -204,7 +229,7 @@ ${c.bold('Build helpers')}
204
229
  stay on yad/wip/…; with ledger: local the epic's ledger rides along
205
230
  yad tidy up [<epic>] [--push] Fold FINISHED Build shards (a shipped story's
206
231
  trust-log/build-log entries) back into the single folded
207
- ledger, as one chore(hub) commit — the manual "pack it up"
232
+ ledger, as one chore(product) commit — the manual "pack it up"
208
233
  for the shard files; a no-op when nothing is foldable
209
234
  yad index [--json] Rebuild .sdlc/index.json — one summary line per work item,
210
235
  derived from their own files; the default branch only, with no
@@ -224,9 +249,14 @@ ${c.bold('Build helpers')}
224
249
  pair-review walkthrough (yad-pair-review)
225
250
  yad review nudge --repo <r> --pr <n> Friendly @-mention on a bare code-PR approve
226
251
  yad review reconcile --epic <id> --repo <r> --pr <n> Bridge: stamp engagement onto the build-log ship
227
- yad repo list Show connected repos (fresh / stale)
252
+ yad repo list Show connected repos (fresh / stale / not cloned here)
253
+ yad repo clone [name] Clone every registered repo missing on this machine, at its
254
+ recorded path (the clone step of \`yad join\`). Never touches a
255
+ repo already there; exits 1 when one could not be cloned
256
+ yad repo sync [name] Switch each connected repo to its default branch and
257
+ fast-forward it from origin; dirty repos are skipped
228
258
  yad repo refresh [name] [--push] Re-pack a stale repo (a human decision). --push commits the
229
- refreshed code-maps + registry as a chore(hub): sync code-context … [skip ci]
259
+ refreshed code-maps + registry as a chore(product): sync code-context … [skip ci]
230
260
  audit commit and pushes it to the Product default branch (--allow-branch to override)
231
261
  yad risk-map check [repo] [--json] Warn where a code repo's .sdlc/risk-map (a risk level per
232
262
  directory, no names) has gone stale — advisory, never blocks
@@ -252,16 +282,23 @@ ${c.bold('Interactive docs (generated sites)')}
252
282
  yad docs sync [--check|--refresh|--wire] Staleness sweep; --wire installs the Pages CI
253
283
 
254
284
  ${c.bold('Options')}
255
- --dir <path> Target project root (default: cwd)
285
+ --dir <path> Target project root. Without it, a Product command finds the Product from
286
+ where you are — any folder inside it, or inside a repo it registers
287
+ (via .yad-workspace.json) — and says so on stderr; a code-repo command
288
+ (commit, open-pr, ship, review) works on the repo you are in
256
289
  --type <t> commit: feat|fix|docs|refactor|test|perf|build|ci|chore|revert
257
290
  -m, --message <s> commit: subject / PR title
258
- --task <id> commit: Task trailer, a <story>-T<NN> id (else derived from the branch)
291
+ --task <id> commit: Task trailer, EP-<slug>-S<n>-T<NN> (else derived from the branch)
259
292
  --ai <id> commit: co-author — claude|copilot|cursor|coderabbit|none (default none)
260
293
  --contract-change commit/open-pr: mark the contract surface touched
294
+ --manual commit: on a verified Product, commit past the local ledger hook; needs
295
+ --reason, recorded as a Ledger-Override trailer. The ledger-guard check
296
+ on the pull request still judges the commit, and quotes the reason
261
297
  --risk <level> open-pr: low|medium|high (default low)
262
298
  --repo <name> open-pr: target a registered repo by name
299
+ --title <text> open-pr/ship: the PR/MR title (ignored on the Product's review/EP-* branch)
263
300
  --base <branch> open-pr: override the PR/MR base — default is the repo's own default
264
- branch (repos.json default_branch, else hub.json default_branch for a PR
301
+ branch (repos.json default_branch, else product.json default_branch for a PR
265
302
  on the Product itself, else the platform, else origin/HEAD, else main); a
266
303
  non-default base loses the AI first pass (warns, never blocks)
267
304
  --epic <id> docs: target one epic's site (EP-<slug>)
@@ -285,7 +322,7 @@ ${c.bold('Environment')}
285
322
  YAD_NO_REPORT=1 Never offer to file a bug report after a failure
286
323
  YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
287
324
 
288
- const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement']);
325
+ const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement', '--keep']);
289
326
 
290
327
  function parseArgs(argv) {
291
328
  const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
@@ -295,6 +332,7 @@ function parseArgs(argv) {
295
332
  if (a === '--fix') o.fix = true;
296
333
  else if (a === '--force') o.force = true;
297
334
  else if (a === '--contract-change') o.contractChange = true;
335
+ else if (a === '--manual') o.manual = true;
298
336
  else if (a === '--no-push') o.noPush = true;
299
337
  else if (a === '--no-fetch') o.noFetch = true;
300
338
  else if (a === '--push') o.push = true;
@@ -314,6 +352,7 @@ function parseArgs(argv) {
314
352
  else if (a === '--debt') o.debt = true;
315
353
  else if (a === '--new-round') o.newRound = true;
316
354
  else if (a === '--hook') o.hook = true;
355
+ else if (a === '--staged') o.staged = true;
317
356
  else if (a === '--stub') o.stub = true;
318
357
  // setup profile flags (pre-answer the Step 0 interview, for CI/scripts)
319
358
  else if (a === '--solo') o.solo = true;
@@ -373,13 +412,46 @@ const ACTIONS = {
373
412
  review: { known: ['trailer', 'context', 'chat', 'cards', 'walkthrough', 'nudge', 'reconcile'] },
374
413
  tidy: { known: ['up'] },
375
414
  history: { known: ['list', 'show', 'search'], default: 'list' },
376
- repo: { known: ['list', 'refresh', 'sync'], default: 'list' },
415
+ repo: { known: ['list', 'refresh', 'sync', 'clone'], default: 'list' },
377
416
  'risk-map': { known: ['check', 'draft'], default: 'check' },
378
417
  codeowners: { known: ['check'], default: 'check' },
379
418
  docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
380
419
  reconcile: { known: ['check', 'refresh', 'wire'] },
381
420
  hook: { known: ['ledger-guard'] },
382
421
  };
422
+ // E80 — how each command finds its folder. A PRODUCT command works on the Product; a REPO command works
423
+ // on the code repo it runs in and reads the Product beside it. The rest take their folder as given:
424
+ // `setup` and `new`/`init`/`join` make one, `report` and `hook` read what is there.
425
+ const PRODUCT_CMDS = new Set([
426
+ 'check', 'update', 'doctor', 'migrate', 'usage', 'sync-status', 'epic', 'foundation', 'skill', 'next',
427
+ 'skip', 'unskip', 'defer', 'undefer', 'dial', 'mode', 'kill', 'unkill', 'unblock', 'gate', 'checkpoint',
428
+ 'capture', 'claims', 'assign', 'unassign', 'owners', 'fold', 'tidy', 'index', 'history', 'repo',
429
+ 'risk-map', 'codeowners', 'docs', 'thread', 'reconcile',
430
+ ]);
431
+ const REPO_CMDS = new Set(['commit', 'open-pr', 'ship', 'review']);
432
+ // The Product commands that WRITE its files without first checking it is one. Run from a code repo they
433
+ // wrote an epic, a dial or the module into it; they refuse instead (E80). The rest either only read, or
434
+ // already refuse a folder that is not a Product.
435
+ function writesProduct(cmd, o) {
436
+ const action = o._[1];
437
+ if (cmd === 'update' || cmd === 'kill' || cmd === 'unkill') return true;
438
+ if (cmd === 'check') return !!o.fix;
439
+ if (cmd === 'epic' || cmd === 'foundation') return action === 'new';
440
+ if (cmd === 'skill') return action === 'bind' || action === 'unbind';
441
+ if (cmd === 'dial') return o.to !== undefined;
442
+ // `--wire` writes a CI workflow; `--refresh` only rebuilds sites the folder already holds.
443
+ if (cmd === 'docs') return action === 'sync' && !!o.wire;
444
+ return false;
445
+ }
446
+
447
+ // Where the person IS, as their shell says it: a repo reached through a link (`ws/backend -> /src/backend`)
448
+ // is inside the workspace by the path they typed, and not by the one the disk resolves to.
449
+ function shellCwd() {
450
+ const pwd = process.env.PWD;
451
+ try { if (pwd && path.isAbsolute(pwd) && fs.realpathSync(pwd) === fs.realpathSync(process.cwd())) return pwd; } catch { /* the disk's path, then */ }
452
+ return process.cwd();
453
+ }
454
+
383
455
  const ALWAYS_JSON = new Set(['gate review', 'gate walkthrough', 'review context', 'review chat', 'review cards', 'review walkthrough']);
384
456
  export function commandName(words) {
385
457
  const [cmd, action] = words;
@@ -423,6 +495,8 @@ async function main() {
423
495
  // so it takes no --json (E1).
424
496
  if (inJSON()) return refuse('yad hook takes no --json — its output is the protocol the agent reads', 'run it without --json');
425
497
  if (action !== 'ledger-guard') return refuse(`unknown hook: ${action ?? '(none)'} (ledger-guard)`);
498
+ // `--staged` is the git pre-commit half (E48): the files come from the index, never from stdin.
499
+ if (o.staged) { runStagedLedgerGuard(); return; }
426
500
  runLedgerGuardHook({ paths: o.path ? [o.path] : [], format: o.format });
427
501
  return;
428
502
  }
@@ -442,16 +516,64 @@ async function main() {
442
516
  const text = helpText(commands.seedableProfiles());
443
517
  return inJSON() ? emitJSON({ help: stripAnsi(text) }) : log(text);
444
518
  }
519
+ // E80: find the Product from wherever yad runs. `--dir` always wins. Otherwise a Product command runs
520
+ // against the Product found by walking up — from any subfolder of the Product, or from inside a repo it
521
+ // registers (through `.yad-workspace.json`) — and says where, on stderr. A code-repo command keeps acting
522
+ // on the repo it runs in, and is handed the Product to read (its registry: name, platform, branch).
523
+ const dirGiven = process.argv.slice(2).some((a) => a === '--dir' || a.startsWith('--dir='));
524
+ if (PRODUCT_CMDS.has(cmd) || REPO_CMDS.has(cmd)) {
525
+ const found = commands.findProduct(dirGiven ? o.dir : shellCwd());
526
+ const shown = (p) => path.relative(process.cwd(), p) || '.';
527
+ if (found?.problem) warn(`${found.problem} — using ${o.dir}`);
528
+ else if (found?.elsewhere) {
529
+ // The workspace folder itself, or a repo the Product does not register: no Product here, but the
530
+ // person is one `cd` away from it — say so, rather than let "run yad setup" make a second one.
531
+ if (PRODUCT_CMDS.has(cmd)) warn(`this folder is not in a repo the Product registers — the Product is ${shown(found.elsewhere)}: cd there, or pass --dir ${shown(found.elsewhere)}`);
532
+ } else if (found?.root && REPO_CMDS.has(cmd)) o.product = found.root;
533
+ else if (found?.root && !dirGiven && found.via !== 'here') {
534
+ o.dir = found.root;
535
+ process.stderr.write(c.dim(`Product: ${shown(found.root)} (${found.via === 'workspace' ? `from ${commands.WORKSPACE_FILE}` : 'above this folder'})\n`));
536
+ // Run from inside one code repo, the per-repo commands work on THAT repo, not on every one the
537
+ // Product registers (they take the repo's name as their second word).
538
+ // The CHECKOUT must be the registered repo (a worktree or nested repo inside it is another one).
539
+ if ((cmd === 'risk-map' || cmd === 'codeowners') && o._[2] === undefined) {
540
+ const hit = commands.registryEntryFor(found.root, dirGiven ? o.dir : shellCwd());
541
+ if (typeof hit?.meta?.name === 'string') o._[2] = hit.meta.name;
542
+ }
543
+ }
544
+ }
545
+ // A command that WRITES the Product never writes into a folder that is not one (a code repo, say).
546
+ // Asked by each such command AFTER its own usage checks, so a mistyped command still hears what is
547
+ // wrong with it first.
548
+ const noProduct = () => {
549
+ if (!writesProduct(cmd, o) || commands.hasProduct(o.dir)) return false;
550
+ refuse(`no Product here (${o.dir}) — yad ${cmd} writes the Product's files`, 'run it from the Product, from inside a repo it registers, or pass --dir <the Product>; `yad new` / `yad init` start one');
551
+ return true;
552
+ };
445
553
  // A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
446
554
  // shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
447
555
  // where the command reports the same thing itself (doctor, migrate) or runs before a project exists.
448
- if (!['hook', 'doctor', 'migrate', 'setup', 'report'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
556
+ if (!['hook', 'doctor', 'migrate', 'setup', 'report', 'new', 'init', 'join'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
557
+ // E122: the Product's settings (or an epic's PR ledger) under two names that say different things.
558
+ // Every command refuses rather than pick one — whichever it picked, somebody's edit would be silently
559
+ // ignored. Not `doctor` (it reports the drift), `migrate` (it ends it) or `report` (it files a bug
560
+ // about a broken flow, and reads nothing it could get wrong). `hook` returned above: it guards.
561
+ if (!['doctor', 'migrate', 'report'].includes(cmd)) {
562
+ // Both the folder the command runs on and, for a code-repo command, the Product it reads.
563
+ for (const root of new Set([o.dir || process.cwd(), o.product].filter(Boolean))) {
564
+ const drift = commands.productDriftError(root);
565
+ if (drift) throw drift;
566
+ }
567
+ }
449
568
 
450
569
  const today = new Date().toISOString().slice(0, 10);
451
570
  // What the command returned: under --json, a command that did not answer itself answers with it.
452
571
  let result;
453
572
  switch (cmd) {
454
573
  case 'setup':
574
+ // A workspace folder holds a Product; it never becomes a second one (E80 review 2 — following
575
+ // "run yad setup" from the workspace folder did exactly that, and hid the real one from its repos).
576
+ if (commands.workspaceProduct(o.dir)) { refuse(`this folder is a workspace — its Product is ${path.relative(process.cwd(), commands.workspaceProduct(o.dir)) || '.'}`, 'run `yad setup` there, or `yad next` from inside it'); break; }
455
577
  result = await commands.runSetup(o.dir, {
456
578
  today, force: o.force,
457
579
  solo: o.solo, team: o.team, greenfield: o.greenfield, brownfield: o.brownfield,
@@ -466,17 +588,36 @@ async function main() {
466
588
  ideTargets: o['ide-targets'] === undefined ? undefined : String(o['ide-targets']).split(',').map((t) => t.trim()).filter(Boolean),
467
589
  });
468
590
  break;
591
+ // E79: the workspace verbs. `--dir` is never the Product here: for `init` it is the WORKSPACE (the
592
+ // folder that holds product/ and the code repos); for `new` and `join` it is the folder the workspace
593
+ // is made IN. The setup flags pass through.
594
+ case 'new':
595
+ case 'init':
596
+ case 'join': {
597
+ const setupFlags = {
598
+ today, force: o.force,
599
+ solo: o.solo, team: o.team, greenfield: o.greenfield, brownfield: o.brownfield,
600
+ monorepo: o.monorepo, separate: o.separate, tools: o.tools,
601
+ ideTargets: o['ide-targets'] === undefined ? undefined : String(o['ide-targets']).split(',').map((t) => t.trim()).filter(Boolean),
602
+ };
603
+ if (cmd === 'new') result = await commands.runNew(o.dir, o._[1], setupFlags);
604
+ else if (cmd === 'init') result = await commands.runInit(o.dir, { ...setupFlags, path: o.path });
605
+ else result = await commands.runJoin(o.dir, o._[1], o._[2], { ideTargets: setupFlags.ideTargets });
606
+ break;
607
+ }
469
608
  case 'check':
609
+ if (noProduct()) break;
470
610
  result = await commands.reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
471
611
  break;
472
612
  case 'update':
613
+ if (noProduct()) break;
473
614
  result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
474
615
  break;
475
616
  case 'doctor':
476
617
  result = await commands.runDoctor(o.dir, { json: o.json });
477
618
  break;
478
619
  case 'migrate':
479
- result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json });
620
+ result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json, keep: o.keep ?? null });
480
621
  break;
481
622
  case 'report':
482
623
  result = await commands.runReport(o.dir, { message: o.message });
@@ -504,6 +645,7 @@ async function main() {
504
645
  refuse(`unknown epic action: ${action ?? '(none)'} (new)`, `usage: yad epic new <slug> [--type feature|chore|change|defect|hotfix] [--profile ${commands.seedableProfiles().join('|')}] [--parent EP-<slug> --inherits <bases>]`);
505
646
  break;
506
647
  }
648
+ if (noProduct()) break;
507
649
  result = await commands.runEpicNew(o.dir, { slug, type: o.type, profile: o.profile, stub: o.stub, parent: o.parent, inherits: o.inherits, today, json: o.json });
508
650
  break;
509
651
  }
@@ -514,12 +656,14 @@ async function main() {
514
656
  refuse(`unknown foundation action: ${action ?? '(none)'} (new, status)`, 'usage: yad foundation new [--json] | yad foundation status [--json]');
515
657
  break;
516
658
  }
659
+ if (noProduct()) break;
517
660
  result = await commands.runFoundationNew(o.dir, { today, json: o.json });
518
661
  break;
519
662
  }
520
663
  case 'skill': {
521
664
  const [, action, step, ...rest] = o._;
522
665
  if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json }); break; }
666
+ if ((action === 'bind' || action === 'unbind') && noProduct()) break;
523
667
  if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest }); break; }
524
668
  if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step }); break; }
525
669
  refuse(`unknown skill action: ${action} (list, bind, unbind)`, 'usage: yad skill list [--json] | yad skill bind <step> <skill> [<skill> ...] | yad skill unbind <step>');
@@ -558,9 +702,11 @@ async function main() {
558
702
  // One word names a Shape author step (project-wide). Three name a Build lane step: epic, story, step.
559
703
  const args = o._.slice(1);
560
704
  if (args.length === 1) {
705
+ if (noProduct()) break;
561
706
  result = await commands.runDial(o.dir, { step: args[0], to: o.to, json: o.json });
562
707
  } else if (args.length === 3) {
563
708
  if (!commands.isValidEpicId(args[0])) { refuse(`invalid epic id: ${args[0]} (expected EP-<slug>, [a-z0-9-] only)`); break; }
709
+ if (noProduct()) break;
564
710
  result = await commands.runDial(o.dir, { epic: args[0], story: args[1], step: args[2], repo: o.repo, to: o.to, json: o.json });
565
711
  } else {
566
712
  refuse('usage: yad dial <step> [--to auto|human] | yad dial <epic> <story> --repo <name> <step> [--to auto|human]');
@@ -575,6 +721,7 @@ async function main() {
575
721
  case 'kill':
576
722
  case 'unkill': {
577
723
  if (o._.length > 1) { refuse(`unexpected argument(s): ${o._.slice(1).join(' ')}`); break; }
724
+ if (noProduct()) break;
578
725
  result = await commands.runKill(o.dir, { on: o._[0] === 'kill', reason: o.reason, json: o.json, today });
579
726
  break;
580
727
  }
@@ -611,26 +758,29 @@ async function main() {
611
758
  }
612
759
  case 'review': {
613
760
  const [, action] = o._;
614
- if (action === 'trailer') result = await commands.reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
615
- else if (action === 'context' || action === 'chat' || action === 'cards') result = await commands.reviewContext(o.dir, { repo: o.repo, pr: o.pr });
616
- else if (action === 'walkthrough') result = await commands.reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
617
- else if (action === 'nudge') result = await commands.reviewNudge(o.dir, { repo: o.repo, pr: o.pr });
761
+ if (action === 'trailer') result = await commands.reviewTrailer(o.dir, { repo: o.repo, product: o.product, pr: o.pr, body: o.body || o.message });
762
+ else if (action === 'context' || action === 'chat' || action === 'cards') result = await commands.reviewContext(o.dir, { repo: o.repo, product: o.product, pr: o.pr });
763
+ else if (action === 'walkthrough') result = await commands.reviewWalkthrough(o.dir, { repo: o.repo, product: o.product, pr: o.pr });
764
+ else if (action === 'nudge') result = await commands.reviewNudge(o.dir, { repo: o.repo, product: o.product, pr: o.pr });
618
765
  else if (action === 'reconcile') {
619
766
  // The epic becomes a path segment under epics/ — reject anything but EP-<slug> (no `../` escape).
620
767
  if (!o.epic || !commands.isValidEpicId(o.epic)) { refuse(`invalid or missing --epic: ${o.epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
621
- result = await commands.reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, pr: o.pr });
768
+ result = await commands.reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, product: o.product, pr: o.pr });
622
769
  }
623
770
  else { refuse('usage: yad review <trailer|context|walkthrough|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]'); }
624
771
  break;
625
772
  }
626
773
  case 'commit':
627
- result = await commands.runCommit(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force });
774
+ result = await commands.runCommit(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, manual: o.manual, reason: o.reason });
628
775
  break;
629
776
  case 'open-pr':
630
- result = await commands.runOpenPr(o.dir, { repo: o.repo, platform: o.platform, base: o.base, title: o.title || o.message, task: o.task, risk: o.risk, contractChange: o.contractChange });
777
+ result = await commands.runOpenPr(o.dir, { repo: o.repo, product: o.product, platform: o.platform, base: o.base, title: o.title || o.message, task: o.task, risk: o.risk, contractChange: o.contractChange });
631
778
  break;
632
779
  case 'ship':
633
- result = await commands.runShip(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, repo: o.repo, platform: o.platform, base: o.base, title: o.title, risk: o.risk });
780
+ // The ledger override is its own act (E49): recorded in its own commit, never folded into a
781
+ // commit-and-open-PR run. Refused, never ignored.
782
+ if (o.manual || o.reason !== undefined) { refuse('yad ship takes no --manual/--reason: commit with `yad commit --manual --reason "<why>" …`, then `yad open-pr`'); break; }
783
+ result = await commands.runShip(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force, repo: o.repo, product: o.product, platform: o.platform, base: o.base, title: o.title, risk: o.risk });
634
784
  break;
635
785
  case 'checkpoint': {
636
786
  let retroShip;
@@ -739,13 +889,14 @@ async function main() {
739
889
  // instead of "unknown command".
740
890
  refuse('yad roster was removed — yadflow keeps no list of people',
741
891
  'A gate needs one approval (not the author\'s own) from anyone with access to the repo; the platform records who approved. '
742
- + 'Request reviewers on the PR itself. Keep an existing `roster` in hub.json until every review with older approvals is closed: '
892
+ + 'Request reviewers on the PR itself. Keep an existing `roster` in the Product settings until every review with older approvals is closed: '
743
893
  + 'it decides nothing, but the first sync uses its name → login pairs to recognise those approvals.');
744
894
  break;
745
895
  case 'docs': {
746
896
  const [, action] = o._;
747
897
  if (o.epic && !commands.isValidEpicId(o.epic)) { refuse(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
748
898
  const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
899
+ if (noProduct()) break;
749
900
  result = await commands.runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
750
901
  break;
751
902
  }
@@ -815,7 +966,10 @@ main()
815
966
  // Require a TTY on BOTH ends: stdout for the message, stdin so the y/N prompt can be answered
816
967
  // (a TTY stdout with piped/closed stdin would otherwise hang on readline).
817
968
  const offerReport = !inJSON() && process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
818
- && !process.env.YAD_NO_REPORT && process.argv[2] !== 'report';
969
+ && !process.env.YAD_NO_REPORT && process.argv[2] !== 'report'
970
+ // Two copies of the project's own settings that disagree (E122) are the project's state, not a
971
+ // yadflow bug: the refusal already names the one command that ends it.
972
+ && err?.code !== 'YAD-STATE-008';
819
973
  if (offerReport) {
820
974
  try {
821
975
  if (await askYesNo('\nReport this failure to the yadflow team?', false)) {
package/cli/capture.mjs CHANGED
@@ -255,12 +255,14 @@ function push(root, git, name, { hook, now, spawner, env }) {
255
255
  const last = Number(state.lastPushAt) || 0;
256
256
  if (now - last < PUSH_EVERY_MS) return { pushed: 'later', why: `the last push started under ${PUSH_EVERY_MS / 60000} minutes ago` };
257
257
  // Recorded BEFORE the push starts, so two hooks a moment apart cannot both start one.
258
+ // `windowsHide` below (E113): on Windows a detached console program gets a console window of its own,
259
+ // so without it every background push would flash one up in front of the person.
258
260
  if (stateFile) writeJSON(stateFile, { ...state, lastPushAt: now });
259
261
  try {
260
- spawner('git', pushArgs(name), { cwd: root, env: { ...env, ...pushEnv(env) }, detached: true, stdio: 'ignore' }).unref();
262
+ spawner('git', pushArgs(name), { cwd: root, env: { ...env, ...pushEnv(env) }, detached: true, stdio: 'ignore', windowsHide: true }).unref();
261
263
  // E46: fetch everyone's capture branches in the same window, so the claim check at the next edit reads
262
264
  // minutes-old claims, not hours-old ones. Detached too: an edit never waits on it, and a failure is silent.
263
- try { spawner('git', fetchAllArgs(), { cwd: root, env: { ...env, ...pushEnv(env) }, detached: true, stdio: 'ignore' }).unref(); } catch { /* claims read what was last fetched */ }
265
+ try { spawner('git', fetchAllArgs(), { cwd: root, env: { ...env, ...pushEnv(env) }, detached: true, stdio: 'ignore', windowsHide: true }).unref(); } catch { /* claims read what was last fetched */ }
264
266
  return { pushed: 'started' };
265
267
  } catch (e) {
266
268
  return { pushed: 'failed', why: e.message };
@@ -2,7 +2,7 @@
2
2
  // build-state) as one audit-trail commit. This is the Build analogue of the Shape gate sync
3
3
  // (cli/gate.mjs): the SDLC's Build part (yad-run, yad-engineer-review) WRITES these ledgers into the
4
4
  // working tree but never commits them, so teammates/CI/`yad status` on other machines see stale trust
5
- // evidence. checkpoint lands them with a `chore(hub): ...` message.
5
+ // evidence. checkpoint lands them with a `chore(product): ...` message.
6
6
  //
7
7
  // It also carries the Build story `status:` flip (approved → in-build/shipped) that
8
8
  // yad-engineer-review writes into stories/<id>.md but no command committed — the #112 drift where
@@ -26,7 +26,7 @@ import { c, log, ok, info, fail, hand, exists, readJSON, readJSONStrict, pushWit
26
26
  import { PROJECT_FILES , productConfigPath } from './manifest.mjs';
27
27
  import { loadProduct } from './gate.mjs';
28
28
  import { platformLogin } from './platform.mjs';
29
- import { productGit, resolveDefaultBranch, guardDefaultBranch } from './hubcommit.mjs';
29
+ import { productGit, resolveDefaultBranch, guardDefaultBranch } from './productcommit.mjs';
30
30
  import { readShips, writeRetroShip } from './ledger.mjs';
31
31
  import { readFrontmatter, declaredRepos } from './epic-state.mjs';
32
32
 
@@ -68,7 +68,7 @@ const BACK_HALF_STATUSES = new Set(['in-build', 'shipped']);
68
68
  // 2. its current frontmatter `status:` is a Build value (in-build | shipped).
69
69
  // A candidate is only actually carried when its staged diff is the `status:` line ALONE — runCheckpoint
70
70
  // drops any candidate whose working tree also changed prose/other frontmatter (stagedStoryIsStatusOnly),
71
- // so an unrelated edit can never ride into a `chore(hub) … [skip ci]` commit that bypasses review.
71
+ // so an unrelated edit can never ride into a `chore(product) … [skip ci]` commit that bypasses review.
72
72
  // The shared `git add`/`diff --cached` machinery then commits ONLY the ones that actually differ from
73
73
  // HEAD (so a story already committed at shipped is a no-op).
74
74
  //
@@ -139,11 +139,11 @@ export function checkpointAuthor(login, name) {
139
139
  }
140
140
 
141
141
  // PURE — the audit-trail commit message. The subject passes the Product commit-message gate (valid type
142
- // `chore`, optional scope `hub`, non-empty description, no trailing period). No Task trailer and no
142
+ // `chore`, optional scope `product`, non-empty description, no trailing period). No Task trailer and no
143
143
  // Co-Authored-By footer: this is human-owned machine state, not an authored code change. `label` and
144
144
  // `author` are collapsed to one line so nothing can split the subject or forge a trailer.
145
145
  export function buildCheckpointMessage({ label, author, basenames = [] }) {
146
- const subject = `chore(hub): sync Build state — ${oneLine(label)} by ${oneLine(author)} [skip ci]`;
146
+ const subject = `chore(product): sync Build state — ${oneLine(label)} by ${oneLine(author)} [skip ci]`;
147
147
  const body = basenames.length ? `Updated: ${basenames.join(', ')}` : '';
148
148
  return body ? `${subject}\n\n${body}` : subject;
149
149
  }
@@ -307,18 +307,18 @@ export async function runCheckpoint(root, opts = {}) {
307
307
  log(c.bold('\nyad checkpoint'));
308
308
  if (!exists(path.join(root, '.git'))) { fail('not a git repo'); process.exitCode = 1; return; }
309
309
  if (!exists(productConfigPath(root))) {
310
- fail('no .sdlc/hub.json — checkpoint commits the Product Build ledger; run it from the Product');
310
+ fail('no .sdlc/product.json — checkpoint commits the Product Build ledger; run it from the Product');
311
311
  process.exitCode = 1;
312
312
  return;
313
313
  }
314
314
 
315
- const { hub } = loadProduct(root);
315
+ const { productConfig } = loadProduct(root);
316
316
  const git = productGit(root);
317
317
  const branch = git('rev-parse', '--abbrev-ref', 'HEAD').stdout;
318
- const defaultBranch = resolveDefaultBranch(git, hub);
318
+ const defaultBranch = resolveDefaultBranch(git, productConfig);
319
319
 
320
320
  // Default-branch guard (invariant 2) — shared with `yad tidy up`.
321
- if (!guardDefaultBranch(branch, defaultBranch, { allowBranch: opts.allowBranch, cmd: 'yad checkpoint' })) return;
321
+ if (!guardDefaultBranch(branch, defaultBranch, { allowBranch: opts.allowBranch, cmd: 'yad checkpoint', root })) return;
322
322
 
323
323
  // --retro-ship (#142): record a retroactive build-log ship for a PRE-TRACKING story (merged before
324
324
  // the Build ledger existed, so it has no ship and its `status:` flip can't be carried). Done
@@ -377,7 +377,7 @@ export async function runCheckpoint(root, opts = {}) {
377
377
  const staged = git('diff', '--cached', '--name-only', '--', ...pathspecs).stdout.split('\n').filter(Boolean);
378
378
 
379
379
  const { label, basenames } = summarizeStaged(staged);
380
- const author = checkpointAuthor(platformLogin(root, hub?.platform), git('config', 'user.name').stdout);
380
+ const author = checkpointAuthor(platformLogin(root, productConfig?.platform), git('config', 'user.name').stdout);
381
381
  const message = buildCheckpointMessage({ label, author, basenames });
382
382
 
383
383
  if (opts.dryRun) {
package/cli/claims.mjs CHANGED
@@ -29,15 +29,15 @@ import fs from 'node:fs';
29
29
  import { ok, info, warn, fail, hand, readJSON, writeJSON } from './lib.mjs';
30
30
  import { productConfigPath } from './manifest.mjs';
31
31
  import { capturedEpic, gitIn, isOwnerPath, pushEnv, wipName, WIP_PREFIX, fetchAllArgs } from './capture.mjs';
32
- import { resolveDefaultBranch } from './hubcommit.mjs';
32
+ import { resolveDefaultBranch } from './productcommit.mjs';
33
33
 
34
34
  export const CLAIM_HOURS = 4;
35
35
  const CLAIM_MS = CLAIM_HOURS * 60 * 60 * 1000;
36
36
 
37
37
  // The default branch as a ref this clone holds — origin's copy first, then a local branch — or null.
38
38
  function defaultRef(root, git) {
39
- const hub = readJSON(productConfigPath(root), {}) || {};
40
- const def = resolveDefaultBranch((...a) => { const r = git(a); return { ok: r.ok, stdout: r.out.trim() }; }, hub);
39
+ const productConfig = readJSON(productConfigPath(root), {}) || {};
40
+ const def = resolveDefaultBranch((...a) => { const r = git(a); return { ok: r.ok, stdout: r.out.trim() }; }, productConfig);
41
41
  for (const ref of [`refs/remotes/origin/${def}`, `refs/heads/${def}`]) if (git(['rev-parse', '--verify', '-q', `${ref}^{commit}`]).ok) return { ref, name: def };
42
42
  return { ref: null, name: def };
43
43
  }
@@ -149,7 +149,7 @@ export async function runClaims(root, { epic = null, noFetch = false, env = proc
149
149
  const opts = { cwd: root, encoding: 'utf8', timeout: 30_000, env: { ...env, ...pushEnv(env) } };
150
150
  const r = spawnSync('git', fetchAllArgs({ prune: true }), opts);
151
151
  fetched = r.status === 0 ? 'done' : 'failed';
152
- // The default branch, for the "landed" rule — on its own: a name guessed wrong (no hub default, no
152
+ // The default branch, for the "landed" rule — on its own: a name guessed wrong (no Product default, no
153
153
  // origin/HEAD) must not stop the capture branches from arriving. Its failure changes nothing.
154
154
  const def = defaultRef(root, git).name;
155
155
  if (fetched === 'done') spawnSync('git', ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=20', 'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${def}:refs/remotes/origin/${def}`], opts);