yadflow 4.0.0-next.1 → 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.
- package/CHANGELOG.md +223 -0
- package/README.md +168 -20
- package/bin/commands.mjs +8 -1
- package/bin/yad.mjs +262 -32
- package/cli/capture.mjs +419 -0
- package/cli/checkpoint.mjs +10 -10
- package/cli/claims.mjs +165 -0
- package/cli/commit.mjs +48 -12
- package/cli/docs.mjs +15 -13
- package/cli/doctor.mjs +764 -131
- package/cli/epic-state.mjs +50 -24
- package/cli/errors.mjs +3 -2
- package/cli/find-product.mjs +197 -0
- package/cli/fold.mjs +247 -0
- package/cli/gate-local.mjs +296 -0
- package/cli/gate.mjs +111 -103
- package/cli/history.mjs +13 -13
- package/cli/hook.mjs +201 -30
- package/cli/index-command.mjs +6 -6
- package/cli/lib.mjs +91 -29
- package/cli/manifest.mjs +213 -98
- package/cli/migrate.mjs +208 -17
- package/cli/mode.mjs +22 -22
- package/cli/next.mjs +30 -10
- package/cli/openpr.mjs +39 -24
- package/cli/owners.mjs +266 -0
- package/cli/people.mjs +26 -11
- package/cli/plan.mjs +798 -50
- package/cli/platform.mjs +13 -7
- package/cli/product-index.mjs +4 -4
- package/cli/{hubcommit.mjs → productcommit.mjs} +14 -9
- package/cli/protection.mjs +19 -9
- package/cli/reconcile.mjs +100 -23
- package/cli/repo-publish.mjs +22 -16
- package/cli/repo.mjs +128 -33
- package/cli/report.mjs +8 -2
- package/cli/review.mjs +26 -19
- package/cli/setup.mjs +112 -35
- package/cli/ship.mjs +1 -1
- package/cli/skip.mjs +4 -4
- package/cli/tidy.mjs +7 -7
- package/cli/update-commit.mjs +22 -19
- package/cli/workspace.mjs +495 -0
- package/package.json +7 -4
- package/skills/sdlc/config.yaml +14 -13
- package/skills/sdlc/module-help.csv +8 -8
- package/skills/yad-analysis/SKILL.md +10 -6
- package/skills/yad-architecture/SKILL.md +7 -5
- package/skills/yad-backfill/SKILL.md +11 -5
- package/skills/yad-backfill/references/backfill.md +37 -0
- package/skills/yad-backfill/templates/checks/backfill-check.sh +208 -17
- package/skills/yad-change/SKILL.md +4 -2
- package/skills/yad-checks/SKILL.md +54 -26
- package/skills/yad-checks/references/check-gates.md +260 -54
- package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
- package/skills/yad-checks/templates/checks/contract-check.sh +534 -20
- package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
- package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
- package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
- package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
- package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
- package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
- package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
- package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
- package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +16 -12
- package/skills/yad-checks/templates/github/yad-update-guard.yml +5 -2
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
- package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +18 -18
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
- package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
- package/skills/yad-commit/SKILL.md +18 -7
- package/skills/yad-connect-design/SKILL.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +1 -1
- package/skills/yad-connect-docs/SKILL.md +16 -11
- package/skills/yad-connect-docs/references/docs-registry.md +8 -8
- package/skills/yad-connect-learning/SKILL.md +1 -1
- package/skills/yad-connect-learning/references/learning-registry.md +1 -1
- package/skills/yad-connect-repos/SKILL.md +31 -8
- package/skills/yad-connect-repos/references/code-context.md +1 -1
- package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
- package/skills/yad-connect-repos/references/repos-registry.md +1 -1
- package/skills/yad-connect-testing/SKILL.md +1 -1
- package/skills/yad-connect-testing/references/testing-registry.md +1 -1
- package/skills/yad-discovery/SKILL.md +4 -3
- package/skills/yad-discovery/references/discovery-schema.md +1 -1
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-docs/references/data-mapping.md +1 -1
- package/skills/yad-docs/templates/app/package-lock.json +8 -8
- package/skills/yad-docs/templates/app/package.json +1 -1
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
- package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
- package/skills/yad-engineer-review/SKILL.md +2 -2
- package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
- package/skills/yad-epic/SKILL.md +11 -6
- package/skills/yad-epic/references/state-schema.md +25 -17
- package/skills/yad-open-pr/SKILL.md +1 -1
- package/skills/yad-pair-review/SKILL.md +5 -5
- package/skills/yad-pair-review/references/session-state.md +2 -2
- package/skills/yad-pr-template/SKILL.md +14 -12
- package/skills/yad-pr-template/templates/checks/pr-template.sh +28 -17
- package/skills/yad-pr-template/templates/checks/pr-title.sh +25 -14
- package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
- package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +17 -16
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
- package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
- package/skills/yad-report/SKILL.md +1 -1
- package/skills/yad-review-companion/SKILL.md +3 -3
- package/skills/yad-review-gate/SKILL.md +97 -107
- package/skills/yad-review-gate/references/gating.md +20 -18
- package/skills/yad-run/SKILL.md +2 -2
- package/skills/yad-run/references/run-loop.md +2 -2
- package/skills/yad-spec/SKILL.md +2 -1
- package/skills/yad-spec/references/spec-handoff.md +19 -10
- package/skills/yad-status/SKILL.md +7 -0
- package/skills/yad-stories/SKILL.md +5 -4
- package/skills/yad-sync-repos/SKILL.md +19 -3
- package/skills/yad-test-cases/SKILL.md +5 -4
- package/skills/yad-ui/SKILL.md +6 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
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.
|
|
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
|
|
@@ -151,6 +170,12 @@ ${c.bold('Review gate (Shape)')}
|
|
|
151
170
|
in verified mode — there, recover with 'yad gate ci' below
|
|
152
171
|
yad gate comments <epic> [artifact] Fetch unresolved review comments to address
|
|
153
172
|
yad gate status <epic> Show each review step + approvals
|
|
173
|
+
yad gate approve <epic> <artifact> --by <name> [--engagement verified|none]
|
|
174
|
+
No platform only: record an approval, bound to the artifact's
|
|
175
|
+
content (an edit revokes it). Reports the verdict; never advances
|
|
176
|
+
yad gate comment <epic> <artifact> --by <name> [--count <n>] [--new-round]
|
|
177
|
+
No platform only: record who commented, and how often, this round
|
|
178
|
+
yad gate advance <epic> <artifact> No platform only: pass the gate when its approvals hold
|
|
154
179
|
yad gate repair <epic> [--push] Close an author step stranded behind a passed review gate
|
|
155
180
|
(YAD-STATE-005); --push commits state.json to the default branch
|
|
156
181
|
yad gate review <epic> [artifact] Print the grounding bundle for the review companion
|
|
@@ -164,26 +189,47 @@ ${c.bold('Review gate (Shape)')}
|
|
|
164
189
|
--merged advances the step + flips artifact status on the default branch
|
|
165
190
|
|
|
166
191
|
${c.bold('Build helpers')}
|
|
167
|
-
yad commit --type <t> -m <subject>
|
|
168
|
-
|
|
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
|
|
169
197
|
hardcoded main; --base overrides) — stage-aware on the Product: a
|
|
170
198
|
review/EP-* branch opens the Shape artifact-review PR
|
|
171
199
|
(delegates to gate open), any other Product branch uses the
|
|
172
|
-
code-task template
|
|
173
|
-
|
|
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)
|
|
174
205
|
yad checkpoint [--push] Commit the machine-written Build state on the Product
|
|
175
206
|
(trust-log/build-log/build-state) — plus any story
|
|
176
207
|
status: flip (→ in-build/shipped) backed by a build-log
|
|
177
|
-
ship — as one audit-trail chore(
|
|
208
|
+
ship — as one audit-trail chore(product) commit; default
|
|
178
209
|
branch only (--allow-branch to override); no-op when clean
|
|
179
210
|
yad checkpoint --retro-ship <epic>/<story> --repo <r>
|
|
180
211
|
Record a retroactive build-log ship for a PRE-TRACKING story
|
|
181
212
|
(merged before ledger tracking), then carry its status: shipped
|
|
182
213
|
flip in the same commit (--merge-commit <sha>, --task <t> opt.);
|
|
183
214
|
one repo per run — re-run per --repo for a multi-repo story
|
|
215
|
+
yad capture [--no-push] Snapshot every changed Shape artifact onto your private
|
|
216
|
+
yad/wip/<you>/<epic> branches — never your checkout; pushes them
|
|
217
|
+
(a harness hook runs it after each agent edit, with --hook)
|
|
218
|
+
yad claims [<epic>] [--no-fetch] Who else is editing which artifact: read from everyone's
|
|
219
|
+
capture branches (fetched first) — advice, not a lock; a claim
|
|
220
|
+
ends 4 hours after the last save, or once the file is merged
|
|
221
|
+
yad assign <epic> <step> [--to <name>] [--force]
|
|
222
|
+
Assign an authoring step to one person (you, or their git
|
|
223
|
+
name) — advice, not a lock: the capture hook warns anyone
|
|
224
|
+
else who edits its files; --force replaces another owner
|
|
225
|
+
yad unassign <epic> <step> [--force] Remove an assignment (someone else's needs --force)
|
|
226
|
+
yad owners [<epic>] List every step assignment, and whether it is live
|
|
227
|
+
yad fold <epic> <step> End an authoring step: commit ITS artifacts as one commit,
|
|
228
|
+
docs(<epic>): author <step> (signed if git signs) — the drafts
|
|
229
|
+
stay on yad/wip/…; with ledger: local the epic's ledger rides along
|
|
184
230
|
yad tidy up [<epic>] [--push] Fold FINISHED Build shards (a shipped story's
|
|
185
231
|
trust-log/build-log entries) back into the single folded
|
|
186
|
-
ledger, as one chore(
|
|
232
|
+
ledger, as one chore(product) commit — the manual "pack it up"
|
|
187
233
|
for the shard files; a no-op when nothing is foldable
|
|
188
234
|
yad index [--json] Rebuild .sdlc/index.json — one summary line per work item,
|
|
189
235
|
derived from their own files; the default branch only, with no
|
|
@@ -203,9 +249,14 @@ ${c.bold('Build helpers')}
|
|
|
203
249
|
pair-review walkthrough (yad-pair-review)
|
|
204
250
|
yad review nudge --repo <r> --pr <n> Friendly @-mention on a bare code-PR approve
|
|
205
251
|
yad review reconcile --epic <id> --repo <r> --pr <n> Bridge: stamp engagement onto the build-log ship
|
|
206
|
-
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
|
|
207
258
|
yad repo refresh [name] [--push] Re-pack a stale repo (a human decision). --push commits the
|
|
208
|
-
refreshed code-maps + registry as a chore(
|
|
259
|
+
refreshed code-maps + registry as a chore(product): sync code-context … [skip ci]
|
|
209
260
|
audit commit and pushes it to the Product default branch (--allow-branch to override)
|
|
210
261
|
yad risk-map check [repo] [--json] Warn where a code repo's .sdlc/risk-map (a risk level per
|
|
211
262
|
directory, no names) has gone stale — advisory, never blocks
|
|
@@ -231,16 +282,23 @@ ${c.bold('Interactive docs (generated sites)')}
|
|
|
231
282
|
yad docs sync [--check|--refresh|--wire] Staleness sweep; --wire installs the Pages CI
|
|
232
283
|
|
|
233
284
|
${c.bold('Options')}
|
|
234
|
-
--dir <path> Target project root
|
|
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
|
|
235
289
|
--type <t> commit: feat|fix|docs|refactor|test|perf|build|ci|chore|revert
|
|
236
290
|
-m, --message <s> commit: subject / PR title
|
|
237
|
-
--task <id> commit: Task trailer,
|
|
291
|
+
--task <id> commit: Task trailer, EP-<slug>-S<n>-T<NN> (else derived from the branch)
|
|
238
292
|
--ai <id> commit: co-author — claude|copilot|cursor|coderabbit|none (default none)
|
|
239
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
|
|
240
297
|
--risk <level> open-pr: low|medium|high (default low)
|
|
241
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)
|
|
242
300
|
--base <branch> open-pr: override the PR/MR base — default is the repo's own default
|
|
243
|
-
branch (repos.json default_branch, else
|
|
301
|
+
branch (repos.json default_branch, else product.json default_branch for a PR
|
|
244
302
|
on the Product itself, else the platform, else origin/HEAD, else main); a
|
|
245
303
|
non-default base loses the AI first pass (warns, never blocks)
|
|
246
304
|
--epic <id> docs: target one epic's site (EP-<slug>)
|
|
@@ -264,7 +322,7 @@ ${c.bold('Environment')}
|
|
|
264
322
|
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
265
323
|
YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
|
|
266
324
|
|
|
267
|
-
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']);
|
|
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']);
|
|
268
326
|
|
|
269
327
|
function parseArgs(argv) {
|
|
270
328
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
@@ -274,7 +332,9 @@ function parseArgs(argv) {
|
|
|
274
332
|
if (a === '--fix') o.fix = true;
|
|
275
333
|
else if (a === '--force') o.force = true;
|
|
276
334
|
else if (a === '--contract-change') o.contractChange = true;
|
|
335
|
+
else if (a === '--manual') o.manual = true;
|
|
277
336
|
else if (a === '--no-push') o.noPush = true;
|
|
337
|
+
else if (a === '--no-fetch') o.noFetch = true;
|
|
278
338
|
else if (a === '--push') o.push = true;
|
|
279
339
|
else if (a === '--allow-branch') o.allowBranch = true;
|
|
280
340
|
else if (a === '--overwrite-local') o.overwriteLocal = true;
|
|
@@ -290,6 +350,9 @@ function parseArgs(argv) {
|
|
|
290
350
|
else if (a === '--done') o.done = true;
|
|
291
351
|
else if (a === '--undo') o.undo = true;
|
|
292
352
|
else if (a === '--debt') o.debt = true;
|
|
353
|
+
else if (a === '--new-round') o.newRound = true;
|
|
354
|
+
else if (a === '--hook') o.hook = true;
|
|
355
|
+
else if (a === '--staged') o.staged = true;
|
|
293
356
|
else if (a === '--stub') o.stub = true;
|
|
294
357
|
// setup profile flags (pre-answer the Step 0 interview, for CI/scripts)
|
|
295
358
|
else if (a === '--solo') o.solo = true;
|
|
@@ -345,17 +408,50 @@ const ACTIONS = {
|
|
|
345
408
|
epic: { known: ['new'] },
|
|
346
409
|
foundation: { known: ['new', 'status'] },
|
|
347
410
|
skill: { known: ['list', 'bind', 'unbind'], default: 'list' },
|
|
348
|
-
gate: { known: ['open', 'sync', 'comments', 'status', 'repair', 'review', 'walkthrough', 'trailer', 'ci'] },
|
|
411
|
+
gate: { known: ['open', 'sync', 'comments', 'status', 'repair', 'review', 'walkthrough', 'trailer', 'ci', 'approve', 'comment', 'advance'] },
|
|
349
412
|
review: { known: ['trailer', 'context', 'chat', 'cards', 'walkthrough', 'nudge', 'reconcile'] },
|
|
350
413
|
tidy: { known: ['up'] },
|
|
351
414
|
history: { known: ['list', 'show', 'search'], default: 'list' },
|
|
352
|
-
repo: { known: ['list', 'refresh', 'sync'], default: 'list' },
|
|
415
|
+
repo: { known: ['list', 'refresh', 'sync', 'clone'], default: 'list' },
|
|
353
416
|
'risk-map': { known: ['check', 'draft'], default: 'check' },
|
|
354
417
|
codeowners: { known: ['check'], default: 'check' },
|
|
355
418
|
docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
|
|
356
419
|
reconcile: { known: ['check', 'refresh', 'wire'] },
|
|
357
420
|
hook: { known: ['ledger-guard'] },
|
|
358
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
|
+
|
|
359
455
|
const ALWAYS_JSON = new Set(['gate review', 'gate walkthrough', 'review context', 'review chat', 'review cards', 'review walkthrough']);
|
|
360
456
|
export function commandName(words) {
|
|
361
457
|
const [cmd, action] = words;
|
|
@@ -399,26 +495,85 @@ async function main() {
|
|
|
399
495
|
// so it takes no --json (E1).
|
|
400
496
|
if (inJSON()) return refuse('yad hook takes no --json — its output is the protocol the agent reads', 'run it without --json');
|
|
401
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; }
|
|
402
500
|
runLedgerGuardHook({ paths: o.path ? [o.path] : [], format: o.format });
|
|
403
501
|
return;
|
|
404
502
|
}
|
|
405
503
|
|
|
504
|
+
// The same hot path for the post-edit capture hook (E43): it runs after every agent edit, never fails,
|
|
505
|
+
// and needs `cli/capture.mjs` alone.
|
|
506
|
+
if (cmd === 'capture' && o.hook) {
|
|
507
|
+
const { runCapture } = await import('../cli/capture.mjs');
|
|
508
|
+
try { await runCapture(o.dir, { hook: true, noPush: !!o.noPush, format: o.format || null }); } catch (e) { process.stderr.write(` • yad capture: ${e.message}\n`); }
|
|
509
|
+
process.exitCode = 0;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
406
513
|
// Every other command. One await, once, for a process that is about to do real work anyway.
|
|
407
514
|
const commands = await import('./commands.mjs');
|
|
408
515
|
if (o.help || !cmd) {
|
|
409
516
|
const text = helpText(commands.seedableProfiles());
|
|
410
517
|
return inJSON() ? emitJSON({ help: stripAnsi(text) }) : log(text);
|
|
411
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
|
+
};
|
|
412
553
|
// A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
|
|
413
554
|
// shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
|
|
414
555
|
// where the command reports the same thing itself (doctor, migrate) or runs before a project exists.
|
|
415
|
-
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
|
+
}
|
|
416
568
|
|
|
417
569
|
const today = new Date().toISOString().slice(0, 10);
|
|
418
570
|
// What the command returned: under --json, a command that did not answer itself answers with it.
|
|
419
571
|
let result;
|
|
420
572
|
switch (cmd) {
|
|
421
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; }
|
|
422
577
|
result = await commands.runSetup(o.dir, {
|
|
423
578
|
today, force: o.force,
|
|
424
579
|
solo: o.solo, team: o.team, greenfield: o.greenfield, brownfield: o.brownfield,
|
|
@@ -433,17 +588,36 @@ async function main() {
|
|
|
433
588
|
ideTargets: o['ide-targets'] === undefined ? undefined : String(o['ide-targets']).split(',').map((t) => t.trim()).filter(Boolean),
|
|
434
589
|
});
|
|
435
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
|
+
}
|
|
436
608
|
case 'check':
|
|
609
|
+
if (noProduct()) break;
|
|
437
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 });
|
|
438
611
|
break;
|
|
439
612
|
case 'update':
|
|
613
|
+
if (noProduct()) break;
|
|
440
614
|
result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
441
615
|
break;
|
|
442
616
|
case 'doctor':
|
|
443
617
|
result = await commands.runDoctor(o.dir, { json: o.json });
|
|
444
618
|
break;
|
|
445
619
|
case 'migrate':
|
|
446
|
-
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 });
|
|
447
621
|
break;
|
|
448
622
|
case 'report':
|
|
449
623
|
result = await commands.runReport(o.dir, { message: o.message });
|
|
@@ -471,6 +645,7 @@ async function main() {
|
|
|
471
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>]`);
|
|
472
646
|
break;
|
|
473
647
|
}
|
|
648
|
+
if (noProduct()) break;
|
|
474
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 });
|
|
475
650
|
break;
|
|
476
651
|
}
|
|
@@ -481,12 +656,14 @@ async function main() {
|
|
|
481
656
|
refuse(`unknown foundation action: ${action ?? '(none)'} (new, status)`, 'usage: yad foundation new [--json] | yad foundation status [--json]');
|
|
482
657
|
break;
|
|
483
658
|
}
|
|
659
|
+
if (noProduct()) break;
|
|
484
660
|
result = await commands.runFoundationNew(o.dir, { today, json: o.json });
|
|
485
661
|
break;
|
|
486
662
|
}
|
|
487
663
|
case 'skill': {
|
|
488
664
|
const [, action, step, ...rest] = o._;
|
|
489
665
|
if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json }); break; }
|
|
666
|
+
if ((action === 'bind' || action === 'unbind') && noProduct()) break;
|
|
490
667
|
if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest }); break; }
|
|
491
668
|
if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step }); break; }
|
|
492
669
|
refuse(`unknown skill action: ${action} (list, bind, unbind)`, 'usage: yad skill list [--json] | yad skill bind <step> <skill> [<skill> ...] | yad skill unbind <step>');
|
|
@@ -525,9 +702,11 @@ async function main() {
|
|
|
525
702
|
// One word names a Shape author step (project-wide). Three name a Build lane step: epic, story, step.
|
|
526
703
|
const args = o._.slice(1);
|
|
527
704
|
if (args.length === 1) {
|
|
705
|
+
if (noProduct()) break;
|
|
528
706
|
result = await commands.runDial(o.dir, { step: args[0], to: o.to, json: o.json });
|
|
529
707
|
} else if (args.length === 3) {
|
|
530
708
|
if (!commands.isValidEpicId(args[0])) { refuse(`invalid epic id: ${args[0]} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
709
|
+
if (noProduct()) break;
|
|
531
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 });
|
|
532
711
|
} else {
|
|
533
712
|
refuse('usage: yad dial <step> [--to auto|human] | yad dial <epic> <story> --repo <name> <step> [--to auto|human]');
|
|
@@ -542,6 +721,7 @@ async function main() {
|
|
|
542
721
|
case 'kill':
|
|
543
722
|
case 'unkill': {
|
|
544
723
|
if (o._.length > 1) { refuse(`unexpected argument(s): ${o._.slice(1).join(' ')}`); break; }
|
|
724
|
+
if (noProduct()) break;
|
|
545
725
|
result = await commands.runKill(o.dir, { on: o._[0] === 'kill', reason: o.reason, json: o.json, today });
|
|
546
726
|
break;
|
|
547
727
|
}
|
|
@@ -555,7 +735,7 @@ async function main() {
|
|
|
555
735
|
const [, action, epic, artifact] = o._;
|
|
556
736
|
// `gate ci` takes no positionals — epic/artifact come from --branch (or a sweep of all PRs).
|
|
557
737
|
if (action === 'ci') { result = await commands.gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
|
|
558
|
-
if (!epic) { refuse('usage: yad gate <open|sync|comments|status|repair|review|walkthrough|trailer|ci> <epic> [artifact]'); break; }
|
|
738
|
+
if (!epic) { refuse('usage: yad gate <open|sync|comments|status|repair|review|walkthrough|trailer|ci|approve|comment|advance> <epic> [artifact]'); break; }
|
|
559
739
|
// The epic id becomes a path segment under epics/ — reject anything but EP-<slug> outright.
|
|
560
740
|
if (!commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
561
741
|
// In verified mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
|
|
@@ -569,31 +749,38 @@ async function main() {
|
|
|
569
749
|
else if (action === 'review') result = await commands.gateReview(o.dir, { epic, artifact });
|
|
570
750
|
else if (action === 'walkthrough') result = await commands.gateWalkthrough(o.dir, { epic, artifact });
|
|
571
751
|
else if (action === 'trailer') result = await commands.gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
|
|
572
|
-
|
|
752
|
+
// E112 — the gate on a Product with no platform: record an approval, record a comment round, advance.
|
|
753
|
+
else if (action === 'approve') result = await commands.gateApprove(o.dir, { epic, artifact, by: o.by, engagement: o.engagement ?? null, today });
|
|
754
|
+
else if (action === 'comment') result = await commands.gateComment(o.dir, { epic, artifact, by: o.by, count: o.count ?? null, newRound: !!o.newRound, today });
|
|
755
|
+
else if (action === 'advance') result = await commands.gateAdvance(o.dir, { epic, artifact, today });
|
|
756
|
+
else { refuse(`unknown gate action: ${action} (open|sync|comments|status|repair|review|walkthrough|trailer|ci|approve|comment|advance)`); }
|
|
573
757
|
break;
|
|
574
758
|
}
|
|
575
759
|
case 'review': {
|
|
576
760
|
const [, action] = o._;
|
|
577
|
-
if (action === 'trailer') result = await commands.reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
|
|
578
|
-
else if (action === 'context' || action === 'chat' || action === 'cards') result = await commands.reviewContext(o.dir, { repo: o.repo, pr: o.pr });
|
|
579
|
-
else if (action === 'walkthrough') result = await commands.reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
|
|
580
|
-
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 });
|
|
581
765
|
else if (action === 'reconcile') {
|
|
582
766
|
// The epic becomes a path segment under epics/ — reject anything but EP-<slug> (no `../` escape).
|
|
583
767
|
if (!o.epic || !commands.isValidEpicId(o.epic)) { refuse(`invalid or missing --epic: ${o.epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
584
|
-
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 });
|
|
585
769
|
}
|
|
586
770
|
else { refuse('usage: yad review <trailer|context|walkthrough|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]'); }
|
|
587
771
|
break;
|
|
588
772
|
}
|
|
589
773
|
case 'commit':
|
|
590
|
-
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 });
|
|
591
775
|
break;
|
|
592
776
|
case 'open-pr':
|
|
593
|
-
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 });
|
|
594
778
|
break;
|
|
595
779
|
case 'ship':
|
|
596
|
-
|
|
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 });
|
|
597
784
|
break;
|
|
598
785
|
case 'checkpoint': {
|
|
599
786
|
let retroShip;
|
|
@@ -609,6 +796,45 @@ async function main() {
|
|
|
609
796
|
result = await commands.runCheckpoint(o.dir, { push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, retroShip });
|
|
610
797
|
break;
|
|
611
798
|
}
|
|
799
|
+
case 'capture': {
|
|
800
|
+
// E43. `--hook` is the harness's post-edit command: it never fails and never prints to stdout.
|
|
801
|
+
if (o._[1]) { refuse(`yad capture takes no word (got ${o._[1]})`, 'usage: yad capture [--no-push]'); break; }
|
|
802
|
+
result = await commands.runCapture(o.dir, { hook: !!o.hook, noPush: !!o.noPush });
|
|
803
|
+
if (o.hook) process.exitCode = 0;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case 'claims': {
|
|
807
|
+
// E46. Read from the capture branches; nothing is recorded here.
|
|
808
|
+
const [, epic, extra] = o._;
|
|
809
|
+
if (extra) { refuse(`yad claims takes at most an epic (got an extra word: ${extra})`, 'usage: yad claims [<epic>] [--no-fetch]'); break; }
|
|
810
|
+
result = await commands.runClaims(o.dir, { epic, noFetch: !!o.noFetch });
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case 'assign':
|
|
814
|
+
case 'unassign': {
|
|
815
|
+
// E47. A file per step; nothing is committed and nothing is blocked.
|
|
816
|
+
const [verb, epic, step, extra] = o._;
|
|
817
|
+
const usage = verb === 'assign' ? 'usage: yad assign <epic> <step> [--to <git name>] [--force]' : 'usage: yad unassign <epic> <step> [--force]';
|
|
818
|
+
if (extra) { refuse(`yad ${verb} takes an epic and a step (got an extra word: ${extra})`, usage); break; }
|
|
819
|
+
if (verb === 'unassign' && o.to != null) { refuse('yad unassign takes no --to: it removes whoever is assigned', usage); break; }
|
|
820
|
+
result = verb === 'assign'
|
|
821
|
+
? await commands.runAssign(o.dir, { epic, step, to: o.to ?? null, force: !!o.force, today })
|
|
822
|
+
: await commands.runUnassign(o.dir, { epic, step, force: !!o.force });
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case 'owners': {
|
|
826
|
+
const [, epic, extra] = o._;
|
|
827
|
+
if (extra) { refuse(`yad owners takes at most an epic (got an extra word: ${extra})`, 'usage: yad owners [<epic>]'); break; }
|
|
828
|
+
result = await commands.runOwners(o.dir, { epic });
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'fold': {
|
|
832
|
+
// E44. The step-boundary commit; the authoring skills run it where they used to say "commit".
|
|
833
|
+
const [, epic, step, extra] = o._;
|
|
834
|
+
if (extra) { refuse(`yad fold takes an epic and a step (got an extra word: ${extra})`, 'usage: yad fold <epic> <step>'); break; }
|
|
835
|
+
result = await commands.runFold(o.dir, { epic, step });
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
612
838
|
case 'tidy': {
|
|
613
839
|
const [, action, epic] = o._;
|
|
614
840
|
if (action !== 'up') {
|
|
@@ -663,13 +889,14 @@ async function main() {
|
|
|
663
889
|
// instead of "unknown command".
|
|
664
890
|
refuse('yad roster was removed — yadflow keeps no list of people',
|
|
665
891
|
'A gate needs one approval (not the author\'s own) from anyone with access to the repo; the platform records who approved. '
|
|
666
|
-
+ 'Request reviewers on the PR itself. Keep an existing `roster` in
|
|
892
|
+
+ 'Request reviewers on the PR itself. Keep an existing `roster` in the Product settings until every review with older approvals is closed: '
|
|
667
893
|
+ 'it decides nothing, but the first sync uses its name → login pairs to recognise those approvals.');
|
|
668
894
|
break;
|
|
669
895
|
case 'docs': {
|
|
670
896
|
const [, action] = o._;
|
|
671
897
|
if (o.epic && !commands.isValidEpicId(o.epic)) { refuse(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
672
898
|
const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
|
|
899
|
+
if (noProduct()) break;
|
|
673
900
|
result = await commands.runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
674
901
|
break;
|
|
675
902
|
}
|
|
@@ -739,7 +966,10 @@ main()
|
|
|
739
966
|
// Require a TTY on BOTH ends: stdout for the message, stdin so the y/N prompt can be answered
|
|
740
967
|
// (a TTY stdout with piped/closed stdin would otherwise hang on readline).
|
|
741
968
|
const offerReport = !inJSON() && process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
|
|
742
|
-
&& !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';
|
|
743
973
|
if (offerReport) {
|
|
744
974
|
try {
|
|
745
975
|
if (await askYesNo('\nReport this failure to the yadflow team?', false)) {
|