yadflow 3.19.0-next.2 → 4.0.0-next.3
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 +371 -0
- package/README.md +160 -22
- package/bin/commands.mjs +46 -0
- package/bin/yad.mjs +506 -117
- package/cli/artifact-status.mjs +30 -11
- package/cli/capture.mjs +417 -0
- package/cli/checkpoint.mjs +47 -27
- package/cli/claims.mjs +165 -0
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1682 -90
- package/cli/epic-state.mjs +3460 -211
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +3 -0
- package/cli/fold.mjs +247 -0
- package/cli/gate-local.mjs +296 -0
- package/cli/gate.mjs +977 -183
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +264 -53
- package/cli/hubcommit.mjs +9 -19
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +54 -4
- package/cli/lib.mjs +161 -9
- package/cli/manifest.mjs +376 -38
- package/cli/migrate.mjs +683 -52
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +365 -86
- package/cli/openpr.mjs +193 -34
- package/cli/owners.mjs +266 -0
- package/cli/people.mjs +657 -0
- package/cli/plan.mjs +466 -125
- package/cli/platform.mjs +109 -128
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +40 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +24 -14
- package/cli/review.mjs +19 -22
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +127 -279
- package/cli/ship.mjs +6 -5
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +41 -16
- package/cli/tidy.mjs +18 -18
- package/cli/update-commit.mjs +21 -21
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +149 -70
- package/skills/sdlc/module-help.csv +24 -24
- package/skills/yad-analysis/SKILL.md +121 -59
- package/skills/yad-architecture/SKILL.md +30 -18
- package/skills/yad-architecture/references/contract-format.md +8 -5
- package/skills/yad-backfill/SKILL.md +10 -4
- package/skills/yad-backfill/references/backfill.md +8 -1
- package/skills/yad-backfill/templates/checks/backfill-check.sh +10 -2
- package/skills/yad-change/SKILL.md +119 -42
- package/skills/yad-change/references/triage.md +42 -27
- package/skills/yad-checks/SKILL.md +68 -35
- package/skills/yad-checks/references/check-gates.md +310 -85
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +130 -9
- package/skills/yad-checks/templates/checks/epic-open.sh +2 -2
- package/skills/yad-checks/templates/checks/ledger-guard.sh +55 -8
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +1 -1
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +13 -2
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +7 -5
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +10 -1
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +3 -3
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +37 -6
- package/skills/yad-checks/templates/hooks/yad-capture.sh +53 -0
- package/skills/yad-commit/SKILL.md +4 -4
- package/skills/yad-connect-design/SKILL.md +5 -5
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +11 -11
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +66 -45
- package/skills/yad-connect-repos/references/code-context.md +4 -4
- package/skills/yad-connect-repos/references/hub-config.md +47 -51
- package/skills/yad-connect-repos/references/repos-registry.md +8 -7
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +5 -5
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +7 -7
- package/skills/yad-discovery/SKILL.md +131 -94
- package/skills/yad-discovery/references/discovery-schema.md +22 -6
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +13 -8
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +30 -15
- package/skills/yad-docs-overview/references/pipeline-model.md +33 -20
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +81 -17
- package/skills/yad-engineer-review/references/ship-and-record.md +17 -8
- package/skills/yad-epic/SKILL.md +173 -92
- package/skills/yad-epic/references/state-schema.md +610 -94
- package/skills/yad-hub-bridge/SKILL.md +57 -38
- package/skills/yad-hub-bridge/references/bridge.md +94 -67
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +21 -11
- package/skills/yad-implement/SKILL.md +27 -13
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +61 -26
- package/skills/yad-pair-review/SKILL.md +7 -5
- package/skills/yad-pair-review/references/session-state.md +1 -1
- package/skills/yad-pr-template/SKILL.md +47 -26
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +34 -11
- package/skills/yad-pr-template/templates/checks/pr-title.sh +32 -9
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +14 -13
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +14 -12
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +6 -3
- package/skills/yad-review-gate/SKILL.md +203 -93
- package/skills/yad-review-gate/references/gating.md +232 -54
- package/skills/yad-run/SKILL.md +81 -51
- package/skills/yad-run/references/run-loop.md +61 -39
- package/skills/yad-ship/SKILL.md +15 -11
- package/skills/yad-spec/SKILL.md +21 -6
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +115 -50
- package/skills/yad-stories/SKILL.md +37 -21
- package/skills/yad-stories/references/story-schema.md +7 -6
- package/skills/yad-stub/SKILL.md +58 -47
- package/skills/yad-sync-repos/SKILL.md +2 -2
- package/skills/yad-test-cases/SKILL.md +32 -23
- package/skills/yad-test-cases/references/test-cases-schema.md +7 -4
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +41 -18
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
package/bin/yad.mjs
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `yad` — setup/maintenance + the PR-driven review gate + build helpers for the SDLC module.
|
|
3
3
|
import { VERSION } from '../cli/manifest.mjs';
|
|
4
|
-
import { c, log, closePrompts, askYesNo } from '../cli/lib.mjs';
|
|
5
|
-
import { runSetup } from '../cli/setup.mjs';
|
|
6
|
-
import { reconcile } from '../cli/reconcile.mjs';
|
|
7
|
-
import { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer, gateWalkthrough, gateRepair } from '../cli/gate.mjs';
|
|
8
|
-
import { isValidEpicId } from '../cli/epic-state.mjs';
|
|
9
|
-
import { runCommit } from '../cli/commit.mjs';
|
|
10
|
-
import { runOpenPr } from '../cli/openpr.mjs';
|
|
11
|
-
import { reviewTrailer, reviewContext, reviewNudge, reviewReconcile, reviewWalkthrough } from '../cli/review.mjs';
|
|
12
|
-
import { runShip } from '../cli/ship.mjs';
|
|
13
|
-
import { runCheckpoint } from '../cli/checkpoint.mjs';
|
|
14
|
-
import { runTidy } from '../cli/tidy.mjs';
|
|
15
|
-
import { runRepo } from '../cli/repo.mjs';
|
|
16
|
-
import { runRoster } from '../cli/roster.mjs';
|
|
17
|
-
import { runDocs } from '../cli/docs.mjs';
|
|
18
|
-
import { runDoctor } from '../cli/doctor.mjs';
|
|
19
|
-
import { runMigrate } from '../cli/migrate.mjs';
|
|
20
|
-
import { runNext } from '../cli/next.mjs';
|
|
21
|
-
import { runSkip } from '../cli/skip.mjs';
|
|
22
|
-
import { syncStatuses } from '../cli/artifact-status.mjs';
|
|
23
|
-
import { runThread, runReconcile } from '../cli/thread.mjs';
|
|
24
|
-
import { runReport } from '../cli/report.mjs';
|
|
25
|
-
import { runUsage } from '../cli/usage.mjs';
|
|
4
|
+
import { c, log, warn, closePrompts, askYesNo, refuse, beginJSON, inJSON, emitJSON, jsonEmitted, jsonFailure, stripAnsi, isPlainObject, ENVELOPE_KEYS } from '../cli/lib.mjs';
|
|
26
5
|
import { runLedgerGuardHook } from '../cli/hook.mjs';
|
|
27
|
-
import { maybeNotifyUpdate } from '../cli/update-notice.mjs';
|
|
28
6
|
|
|
29
|
-
const
|
|
7
|
+
const helpText = (profiles) => `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
8
|
+
|
|
9
|
+
${c.dim('Every command but `yad hook` takes --json: one JSON object on stdout, every other line on stderr')}
|
|
10
|
+
${c.dim('(docs/CLI.md, "--json on every command").')}
|
|
30
11
|
|
|
31
12
|
${c.bold('Setup & maintenance')}
|
|
32
13
|
yad setup Guided first-run setup (profile interview, install, connect & wire repos)
|
|
@@ -42,7 +23,7 @@ ${c.bold('Setup & maintenance')}
|
|
|
42
23
|
it. Anything else it cannot account for is replaced only after
|
|
43
24
|
a <file>.yad-orig backup
|
|
44
25
|
yad update --push Also commit each repo's applied changes and push them straight to the
|
|
45
|
-
default branch of the
|
|
26
|
+
default branch of the Product + every connected repo (a chore(yad-update)
|
|
46
27
|
commit; no PR — the push-on-main yad-update-guard runs verified-commits
|
|
47
28
|
+ commit-message). Announce the team & pause merges first. Also works as
|
|
48
29
|
'yad check --fix --push'; --allow-branch permits a non-default branch
|
|
@@ -61,16 +42,11 @@ ${c.bold('Setup & maintenance')}
|
|
|
61
42
|
yad hook ledger-guard ${c.dim('harness-invoked, not typed')} — refuse an agent's edit to the
|
|
62
43
|
CI-owned gate ledger in verified mode and name the command that owns
|
|
63
44
|
the transition. Reads a tool-call payload on stdin (or --path <p>);
|
|
64
|
-
exit 0 allows, exit 2 denies with the reason on stderr.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
yad roster add <login> Add/edit a member, then walk the connected repos for their roles
|
|
70
|
-
(--name, --email, --roles "hub=owner,reviewer backend=domain-owner")
|
|
71
|
-
yad roster grant <name> <repo> <role...> Grant role(s) for a connected repo (domain-owner|reviewer|owner)
|
|
72
|
-
yad roster revoke <name> <repo> <role...> Remove role(s) for a repo
|
|
73
|
-
yad roster remove <login> Delete a member from the roster
|
|
45
|
+
exit 0 allows, exit 2 denies with the reason on stderr. With
|
|
46
|
+
--format cursor the verdict is JSON on stdout instead, for a harness
|
|
47
|
+
whose pre-edit hook asks for permission rather than reading an exit
|
|
48
|
+
code. Wired into .claude/settings.json and .cursor/hooks.json by
|
|
49
|
+
setup / check --fix. YAD_HOOK_DISABLE=1 skips.
|
|
74
50
|
|
|
75
51
|
${c.bold('Team usage (EM adoption & behavior report)')}
|
|
76
52
|
yad usage Build a per-member report (HTML) from git + the SDLC ledgers
|
|
@@ -80,16 +56,90 @@ ${c.bold('Team usage (EM adoption & behavior report)')}
|
|
|
80
56
|
--member <name>, --format html|json|md, --repos (code commits)
|
|
81
57
|
|
|
82
58
|
${c.bold('Where am I / what next')}
|
|
59
|
+
yad epic new <slug> [--type <t>] [--profile <p>] [--stub] [--parent <epic> --inherits <bases>] [--json]
|
|
60
|
+
Seed a new epic's lifecycle: writes its step chain from a
|
|
61
|
+
profile, plus empty approval/comment ledgers and reviews/.
|
|
62
|
+
--type feature|chore (default feature). A change/defect/
|
|
63
|
+
hotfix needs --parent: it takes the parent's route, and
|
|
64
|
+
--inherits epic,architecture,contract,ui-design carries
|
|
65
|
+
those steps by reference (satisfied, bound to the owner's
|
|
66
|
+
hash). Carrying the contract writes a pointer-lock instead
|
|
67
|
+
of a second contract (E42). The yad-change skill triages.
|
|
68
|
+
--profile ${profiles.join('|')} (default classic).
|
|
69
|
+
classic and analysis-first are the full chains; chore and
|
|
70
|
+
spike are E40's short lanes — epic + stories, with the
|
|
71
|
+
analyst's brief in front for a spike. Neither carries an
|
|
72
|
+
architecture gate, so neither may move the contract surface.
|
|
73
|
+
--stub seeds a brownfield anchor instead: the same chain
|
|
74
|
+
with every step blocked behind backfill-pending, so a
|
|
75
|
+
defect can thread off a feature that shipped before the
|
|
76
|
+
Product existed (yad-backfill promote wakes it).
|
|
77
|
+
Writes no epic.md, no branch and no commit: run the skill
|
|
78
|
+
the chain names next. Refuses an epic that already has one
|
|
79
|
+
yad foundation new [--json] Seed the Product level (E75): the Foundation's two-step chain
|
|
80
|
+
in foundation/.sdlc/, plus empty ledgers and reviews/. Run once
|
|
81
|
+
per product, before any epic — then the yad-discovery skill
|
|
82
|
+
authors its sections. Refuses a second one, and a product still
|
|
83
|
+
on the old epics/EP-discovery/ (yad migrate converts that)
|
|
84
|
+
yad foundation status [--json] Which roadmap features are started: reads each proposed epic id
|
|
85
|
+
in roadmap.md and reports planned / in-shape / in-build /
|
|
86
|
+
shipped from the epic ledgers. Read-only — the roadmap's Status
|
|
87
|
+
column is no longer kept by hand
|
|
83
88
|
yad next Project-wide: the one next action to take (or run setup)
|
|
84
89
|
yad next <epic> The single next action for one epic (skill or yad command)
|
|
85
90
|
yad next <epic> --check <step> Exit 0 if <step> is runnable now, else 1 (precondition guard)
|
|
86
91
|
yad next --all Every active epic's next action at once
|
|
87
92
|
yad next [<epic>] --json The same answer as a machine-readable action object (for
|
|
88
93
|
agents/CI) — always every epic, so --all is implied
|
|
89
|
-
yad
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
yad skill list [--json] Which skill runs which step, and whether that is this
|
|
95
|
+
project's choice or the engine's default
|
|
96
|
+
yad skill bind <step> <skill> [<skill> ...]
|
|
97
|
+
Bind a step to a skill of your own. Several skills run in
|
|
98
|
+
the order given, one after another — each costs tokens
|
|
99
|
+
yad skill unbind <step> Drop the binding; the step goes back to the engine's default
|
|
100
|
+
yad skip <epic> <step> --reason <text> Mark an optional step N/A for this epic. Which steps
|
|
101
|
+
those are comes from the epic's lifecycle route — on classic
|
|
102
|
+
and analysis-first, ui-design: a backend/API/data epic with no
|
|
103
|
+
UI. Stays visible & auditable (reason recorded, gate
|
|
104
|
+
short-circuited); refused once any later step has started
|
|
105
|
+
yad unskip <epic> <step> Put a skipped step back in the chain (\`skip --undo\` does the
|
|
106
|
+
same), until the step that follows it is finished or work
|
|
107
|
+
past it starts — on classic, until stories are done
|
|
108
|
+
yad skip <epic> <story> --repo <name> --reason <text>
|
|
109
|
+
Skip a whole Build lane: this story needs no change in this
|
|
110
|
+
repo (E39). Written to build-state/<story>.json; commit it with
|
|
111
|
+
yad checkpoint --push. Refused before the stories review passes
|
|
112
|
+
(edit the story's repos: instead), for a repo the story does not
|
|
113
|
+
declare, once work started or a ship is recorded, and for the
|
|
114
|
+
story's last lane. No single Build step can be skipped
|
|
115
|
+
yad unskip <epic> <story> --repo <name>
|
|
116
|
+
Put a skipped Build lane back; yad-run adds it on its next run
|
|
117
|
+
yad defer <epic> <step> --reason <text> [--debt]
|
|
118
|
+
Set an optional step aside to do LATER: marks it deferred.
|
|
119
|
+
Say who is waiting for it in the reason. Same steps and same
|
|
120
|
+
refusals as skip; the chain goes on, its review is still owed.
|
|
121
|
+
--debt marks it owed back: yad next and yad doctor remind
|
|
122
|
+
you until its review passes
|
|
123
|
+
yad undefer <epic> <step> Put a deferred step back, at any time. After later work has
|
|
124
|
+
finished, the step re-opens beside that work, which stays done
|
|
125
|
+
yad unblock <epic> <step> Clear a recorded blocker once the wait is over: moves the
|
|
126
|
+
step off blocked and removes its record in one write
|
|
127
|
+
yad dial <step> [--to auto|human] [--json]
|
|
128
|
+
A Shape author step's advance dial, for the whole project (E34),
|
|
129
|
+
kept in .sdlc/automation.json. No --to shows it. A review gate is
|
|
130
|
+
always human. Recorded only for now: nothing drives a Shape step
|
|
131
|
+
on its own until the engine runs agents
|
|
132
|
+
yad dial <epic> <story> --repo <name> <step> [--to auto|human] [--json]
|
|
133
|
+
A Build lane step's dial (spec, tasks, implement, checks), in
|
|
134
|
+
build-state; on auto the yad-run skill moves past it after a clean
|
|
135
|
+
run. Shows the step's run record as advice, never as a rule
|
|
136
|
+
yad kill --reason <text> Kill switch: hold every step at advance: human. Recorded in
|
|
137
|
+
.sdlc/automation.json — who, when and why
|
|
138
|
+
yad unkill [--reason <text>] Turn the kill switch off; each step follows its own dial again
|
|
139
|
+
yad mode [solo --reason <text> | team [--reason <text>]] [--json]
|
|
140
|
+
Who must approve. Solo waives approvals on every review gate (the
|
|
141
|
+
merge still decides); team counts them. Records who, when and why.
|
|
142
|
+
No word reads the mode. Open reviews follow it from their next sync
|
|
93
143
|
|
|
94
144
|
${c.bold('Review gate (Shape)')}
|
|
95
145
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review. The review
|
|
@@ -101,6 +151,12 @@ ${c.bold('Review gate (Shape)')}
|
|
|
101
151
|
in verified mode — there, recover with 'yad gate ci' below
|
|
102
152
|
yad gate comments <epic> [artifact] Fetch unresolved review comments to address
|
|
103
153
|
yad gate status <epic> Show each review step + approvals
|
|
154
|
+
yad gate approve <epic> <artifact> --by <name> [--engagement verified|none]
|
|
155
|
+
No platform only: record an approval, bound to the artifact's
|
|
156
|
+
content (an edit revokes it). Reports the verdict; never advances
|
|
157
|
+
yad gate comment <epic> <artifact> --by <name> [--count <n>] [--new-round]
|
|
158
|
+
No platform only: record who commented, and how often, this round
|
|
159
|
+
yad gate advance <epic> <artifact> No platform only: pass the gate when its approvals hold
|
|
104
160
|
yad gate repair <epic> [--push] Close an author step stranded behind a passed review gate
|
|
105
161
|
(YAD-STATE-005); --push commits state.json to the default branch
|
|
106
162
|
yad gate review <epic> [artifact] Print the grounding bundle for the review companion
|
|
@@ -110,18 +166,18 @@ ${c.bold('Review gate (Shape)')}
|
|
|
110
166
|
yad gate trailer <epic> [artifact] --body <text> [--pr <n>]
|
|
111
167
|
Upsert the companion's 60-sec briefing into the PR/MR description
|
|
112
168
|
yad gate ci [--branch <head>] [--pr <n>] [--merged]
|
|
113
|
-
CI entry (
|
|
169
|
+
CI entry (Product workflow): pre-merge is read-only (nothing pushed);
|
|
114
170
|
--merged advances the step + flips artifact status on the default branch
|
|
115
171
|
|
|
116
172
|
${c.bold('Build helpers')}
|
|
117
173
|
yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
|
|
118
174
|
yad open-pr [--repo <name>] Open a task PR/MR against the repo's DEFAULT branch (never a
|
|
119
|
-
hardcoded main; --base overrides) — stage-aware on the
|
|
175
|
+
hardcoded main; --base overrides) — stage-aware on the Product: a
|
|
120
176
|
review/EP-* branch opens the Shape artifact-review PR
|
|
121
|
-
(delegates to gate open), any other
|
|
177
|
+
(delegates to gate open), any other Product branch uses the
|
|
122
178
|
code-task template
|
|
123
179
|
yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
|
|
124
|
-
yad checkpoint [--push] Commit the machine-written Build
|
|
180
|
+
yad checkpoint [--push] Commit the machine-written Build state on the Product
|
|
125
181
|
(trust-log/build-log/build-state) — plus any story
|
|
126
182
|
status: flip (→ in-build/shipped) backed by a build-log
|
|
127
183
|
ship — as one audit-trail chore(hub) commit; default
|
|
@@ -131,10 +187,37 @@ ${c.bold('Build helpers')}
|
|
|
131
187
|
(merged before ledger tracking), then carry its status: shipped
|
|
132
188
|
flip in the same commit (--merge-commit <sha>, --task <t> opt.);
|
|
133
189
|
one repo per run — re-run per --repo for a multi-repo story
|
|
190
|
+
yad capture [--no-push] Snapshot every changed Shape artifact onto your private
|
|
191
|
+
yad/wip/<you>/<epic> branches — never your checkout; pushes them
|
|
192
|
+
(a harness hook runs it after each agent edit, with --hook)
|
|
193
|
+
yad claims [<epic>] [--no-fetch] Who else is editing which artifact: read from everyone's
|
|
194
|
+
capture branches (fetched first) — advice, not a lock; a claim
|
|
195
|
+
ends 4 hours after the last save, or once the file is merged
|
|
196
|
+
yad assign <epic> <step> [--to <name>] [--force]
|
|
197
|
+
Assign an authoring step to one person (you, or their git
|
|
198
|
+
name) — advice, not a lock: the capture hook warns anyone
|
|
199
|
+
else who edits its files; --force replaces another owner
|
|
200
|
+
yad unassign <epic> <step> [--force] Remove an assignment (someone else's needs --force)
|
|
201
|
+
yad owners [<epic>] List every step assignment, and whether it is live
|
|
202
|
+
yad fold <epic> <step> End an authoring step: commit ITS artifacts as one commit,
|
|
203
|
+
docs(<epic>): author <step> (signed if git signs) — the drafts
|
|
204
|
+
stay on yad/wip/…; with ledger: local the epic's ledger rides along
|
|
134
205
|
yad tidy up [<epic>] [--push] Fold FINISHED Build shards (a shipped story's
|
|
135
206
|
trust-log/build-log entries) back into the single folded
|
|
136
207
|
ledger, as one chore(hub) commit — the manual "pack it up"
|
|
137
208
|
for the shard files; a no-op when nothing is foldable
|
|
209
|
+
yad index [--json] Rebuild .sdlc/index.json — one summary line per work item,
|
|
210
|
+
derived from their own files; the default branch only, with no
|
|
211
|
+
override; on a verified Product CI writes it.
|
|
212
|
+
--json prints it, built live, and writes nothing
|
|
213
|
+
yad history [list] [--type t] [--theme x] [--thread EP-…] [--open|--done] [--json]
|
|
214
|
+
Every work item, open and shape-done (Build is not counted),
|
|
215
|
+
newest first — built live from their own files; writes nothing
|
|
216
|
+
yad history show <id> [--json] One work item: every step with its state and closing record,
|
|
217
|
+
and the approvals recorded for each review step
|
|
218
|
+
yad history search <text> [filters] [--json]
|
|
219
|
+
Match text in ids, titles, themes, types, repos, and in who
|
|
220
|
+
closed or merged a step, its PR and its commit
|
|
138
221
|
yad review trailer --repo <r> --pr <n> --body <text> Post the companion's 60-sec briefing to a code PR/MR
|
|
139
222
|
yad review context --repo <r> --pr <n> Print the grounding bundle for cards/chat
|
|
140
223
|
yad review walkthrough --repo <r> --pr <n> Bundle + ordered risk-tagged stops for the
|
|
@@ -144,7 +227,17 @@ ${c.bold('Build helpers')}
|
|
|
144
227
|
yad repo list Show connected repos (fresh / stale)
|
|
145
228
|
yad repo refresh [name] [--push] Re-pack a stale repo (a human decision). --push commits the
|
|
146
229
|
refreshed code-maps + registry as a chore(hub): sync code-context … [skip ci]
|
|
147
|
-
audit commit and pushes it to the
|
|
230
|
+
audit commit and pushes it to the Product default branch (--allow-branch to override)
|
|
231
|
+
yad risk-map check [repo] [--json] Warn where a code repo's .sdlc/risk-map (a risk level per
|
|
232
|
+
directory, no names) has gone stale — advisory, never blocks
|
|
233
|
+
yad risk-map draft [repo] Add an 'unset' line for every directory the map does not cover
|
|
234
|
+
(--dry-run prints it);
|
|
235
|
+
never changes a line (the yad-connect-repos skill classifies them)
|
|
236
|
+
yad codeowners check [repo] [--json] [--platform github|gitlab]
|
|
237
|
+
Warn where a code repo's CODEOWNERS looks stale: a line that
|
|
238
|
+
matches no file, a file the platform never reads or will not load,
|
|
239
|
+
a line yad cannot read; plus a hint about @logins with no recent
|
|
240
|
+
commit — advisory, never blocks, never writes the file
|
|
148
241
|
|
|
149
242
|
${c.bold('Feature threads (post-lock change management)')}
|
|
150
243
|
yad thread List every feature thread (genesis → changes → defects)
|
|
@@ -169,7 +262,7 @@ ${c.bold('Options')}
|
|
|
169
262
|
--repo <name> open-pr: target a registered repo by name
|
|
170
263
|
--base <branch> open-pr: override the PR/MR base — default is the repo's own default
|
|
171
264
|
branch (repos.json default_branch, else hub.json default_branch for a PR
|
|
172
|
-
on the
|
|
265
|
+
on the Product itself, else the platform, else origin/HEAD, else main); a
|
|
173
266
|
non-default base loses the AI first pass (warns, never blocks)
|
|
174
267
|
--epic <id> docs: target one epic's site (EP-<slug>)
|
|
175
268
|
--overview docs: target the project SDLC-overview site
|
|
@@ -189,18 +282,21 @@ ${c.bold('Options')}
|
|
|
189
282
|
|
|
190
283
|
${c.bold('Environment')}
|
|
191
284
|
YAD_NO_UPDATE_NOTIFIER=1 Silence the "update available" notice (also off in CI)
|
|
192
|
-
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
285
|
+
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
286
|
+
YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
|
|
193
287
|
|
|
194
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--
|
|
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']);
|
|
195
289
|
|
|
196
290
|
function parseArgs(argv) {
|
|
197
291
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
292
|
+
try {
|
|
198
293
|
for (let i = 0; i < argv.length; i++) {
|
|
199
294
|
const a = argv[i];
|
|
200
295
|
if (a === '--fix') o.fix = true;
|
|
201
296
|
else if (a === '--force') o.force = true;
|
|
202
297
|
else if (a === '--contract-change') o.contractChange = true;
|
|
203
298
|
else if (a === '--no-push') o.noPush = true;
|
|
299
|
+
else if (a === '--no-fetch') o.noFetch = true;
|
|
204
300
|
else if (a === '--push') o.push = true;
|
|
205
301
|
else if (a === '--allow-branch') o.allowBranch = true;
|
|
206
302
|
else if (a === '--overwrite-local') o.overwriteLocal = true;
|
|
@@ -212,7 +308,13 @@ function parseArgs(argv) {
|
|
|
212
308
|
// positional. `o._[0]` is the command, already pushed by the time `--check` is seen in normal use.
|
|
213
309
|
else if (a === '--check') { const v = argv[i + 1]; o.check = (o._[0] === 'next' && v !== undefined && !v.startsWith('-')) ? argv[++i] : true; }
|
|
214
310
|
else if (a === '--all') o.all = true;
|
|
311
|
+
else if (a === '--open') o.open = true;
|
|
312
|
+
else if (a === '--done') o.done = true;
|
|
215
313
|
else if (a === '--undo') o.undo = true;
|
|
314
|
+
else if (a === '--debt') o.debt = true;
|
|
315
|
+
else if (a === '--new-round') o.newRound = true;
|
|
316
|
+
else if (a === '--hook') o.hook = true;
|
|
317
|
+
else if (a === '--stub') o.stub = true;
|
|
216
318
|
// setup profile flags (pre-answer the Step 0 interview, for CI/scripts)
|
|
217
319
|
else if (a === '--solo') o.solo = true;
|
|
218
320
|
else if (a === '--greenfield') o.greenfield = true;
|
|
@@ -234,11 +336,60 @@ function parseArgs(argv) {
|
|
|
234
336
|
else if (a.startsWith('--scope=')) o.scope = a.slice('--scope='.length);
|
|
235
337
|
else if (a === '-m' || a === '--message') o.message = takeValue(argv, ++i, a);
|
|
236
338
|
else if (VALUE_FLAGS.has(a)) o[a.replace(/^--/, '')] = takeValue(argv, ++i, a);
|
|
237
|
-
|
|
339
|
+
// `--flag=value` for the same flags. Without it the token matched nothing and fell through to the
|
|
340
|
+
// positionals, where it was silently ignored — and for `yad hook ledger-guard --format=cursor`
|
|
341
|
+
// that is not a cosmetic miss: the format goes undefined, the exit protocol is selected, and under
|
|
342
|
+
// Cursor an empty stdout on an allow BLOCKS every file write. A flag spelling that silently
|
|
343
|
+
// inverts a guard is worth accepting rather than quietly dropping.
|
|
344
|
+
else if (a.startsWith('--') && a.includes('=') && VALUE_FLAGS.has(a.slice(0, a.indexOf('=')))) {
|
|
345
|
+
const eq = a.indexOf('=');
|
|
346
|
+
const value = a.slice(eq + 1);
|
|
347
|
+
if (!value) throw new Error(`${a.slice(0, eq)} expects a value`);
|
|
348
|
+
o[a.slice(2, eq)] = value;
|
|
349
|
+
} else o._.push(a);
|
|
350
|
+
}
|
|
351
|
+
} catch (e) {
|
|
352
|
+
// The command read so far, so the error handler knows WHICH command failed — the parse did not finish.
|
|
353
|
+
e.parsedCmd = o._[0] ?? null;
|
|
354
|
+
// The words read so far, so a refusal names `history show`, not the default action (E1 review).
|
|
355
|
+
e.parsedWords = o._.slice(0, 2);
|
|
356
|
+
throw e;
|
|
238
357
|
}
|
|
239
358
|
return o;
|
|
240
359
|
}
|
|
241
360
|
|
|
361
|
+
// The command `main` is running, for the error handler (set once the arguments are read).
|
|
362
|
+
let runningCmd = null;
|
|
363
|
+
|
|
364
|
+
// The `command` a --json answer names: the command word, plus its action where it has one, as the
|
|
365
|
+
// user would type it (`gate status`, `history show`). An action left out is the one that ran
|
|
366
|
+
// (`yad skill` is `skill list`); a word that is not one of the command's actions is not added, so a
|
|
367
|
+
// refusal of `yad gate frobnicate` names `gate`, not a command that does not exist.
|
|
368
|
+
const ACTIONS = {
|
|
369
|
+
epic: { known: ['new'] },
|
|
370
|
+
foundation: { known: ['new', 'status'] },
|
|
371
|
+
skill: { known: ['list', 'bind', 'unbind'], default: 'list' },
|
|
372
|
+
gate: { known: ['open', 'sync', 'comments', 'status', 'repair', 'review', 'walkthrough', 'trailer', 'ci', 'approve', 'comment', 'advance'] },
|
|
373
|
+
review: { known: ['trailer', 'context', 'chat', 'cards', 'walkthrough', 'nudge', 'reconcile'] },
|
|
374
|
+
tidy: { known: ['up'] },
|
|
375
|
+
history: { known: ['list', 'show', 'search'], default: 'list' },
|
|
376
|
+
repo: { known: ['list', 'refresh', 'sync'], default: 'list' },
|
|
377
|
+
'risk-map': { known: ['check', 'draft'], default: 'check' },
|
|
378
|
+
codeowners: { known: ['check'], default: 'check' },
|
|
379
|
+
docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
|
|
380
|
+
reconcile: { known: ['check', 'refresh', 'wire'] },
|
|
381
|
+
hook: { known: ['ledger-guard'] },
|
|
382
|
+
};
|
|
383
|
+
const ALWAYS_JSON = new Set(['gate review', 'gate walkthrough', 'review context', 'review chat', 'review cards', 'review walkthrough']);
|
|
384
|
+
export function commandName(words) {
|
|
385
|
+
const [cmd, action] = words;
|
|
386
|
+
if (!cmd) return null;
|
|
387
|
+
const a = ACTIONS[cmd];
|
|
388
|
+
if (!a) return cmd;
|
|
389
|
+
if (action === undefined) return a.default ? `${cmd} ${a.default}` : cmd;
|
|
390
|
+
return a.known.includes(action) ? `${cmd} ${action}` : cmd;
|
|
391
|
+
}
|
|
392
|
+
|
|
242
393
|
// A value flag must be followed by a token; erroring beats silently passing `undefined` downstream.
|
|
243
394
|
function takeValue(argv, i, flag) {
|
|
244
395
|
const v = argv[i];
|
|
@@ -247,179 +398,413 @@ function takeValue(argv, i, flag) {
|
|
|
247
398
|
}
|
|
248
399
|
|
|
249
400
|
async function main() {
|
|
401
|
+
// `--json` is read from the raw arguments too, so a refusal from the parser itself (a flag with no
|
|
402
|
+
// value) still answers as one object.
|
|
403
|
+
if (process.argv.slice(2).includes('--json')) beginJSON(null);
|
|
250
404
|
const o = parseArgs(process.argv.slice(2));
|
|
251
405
|
const cmd = o._[0];
|
|
252
|
-
|
|
253
|
-
|
|
406
|
+
runningCmd = cmd ?? null;
|
|
407
|
+
// The grounding bundles a skill parses are JSON with or without the flag, so they are always a
|
|
408
|
+
// --json run: one object on stdout, a refusal included, and every other line on stderr.
|
|
409
|
+
if (o.json || ALWAYS_JSON.has(commandName(o._))) beginJSON(commandName(o._));
|
|
410
|
+
if (o.version) return inJSON() ? emitJSON({}) : log(VERSION);
|
|
411
|
+
|
|
412
|
+
// THE HOT PATH, handled before anything heavy is loaded. `yad hook ledger-guard` runs inside the
|
|
413
|
+
// agent's tool loop on every file-editing call, and it needs `cli/hook.mjs` alone. Everything else
|
|
414
|
+
// lives behind the single dynamic import below, so the hook no longer pays for 29 modules it does
|
|
415
|
+
// not use — measured at ~30ms of module loading against ~14ms for what it actually needs.
|
|
416
|
+
//
|
|
417
|
+
// It sits above the help and the newer-shape warning deliberately: `--help` is not a hook call, and
|
|
418
|
+
// the warning is already excluded for `hook` below because its stderr is the channel a block reason
|
|
419
|
+
// reaches the model on.
|
|
420
|
+
if (cmd === 'hook') {
|
|
421
|
+
const [, action] = o._;
|
|
422
|
+
// Its stdout is already a protocol (Claude Code reads the exit code, Cursor a permission object),
|
|
423
|
+
// so it takes no --json (E1).
|
|
424
|
+
if (inJSON()) return refuse('yad hook takes no --json — its output is the protocol the agent reads', 'run it without --json');
|
|
425
|
+
if (action !== 'ledger-guard') return refuse(`unknown hook: ${action ?? '(none)'} (ledger-guard)`);
|
|
426
|
+
runLedgerGuardHook({ paths: o.path ? [o.path] : [], format: o.format });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The same hot path for the post-edit capture hook (E43): it runs after every agent edit, never fails,
|
|
431
|
+
// and needs `cli/capture.mjs` alone.
|
|
432
|
+
if (cmd === 'capture' && o.hook) {
|
|
433
|
+
const { runCapture } = await import('../cli/capture.mjs');
|
|
434
|
+
try { await runCapture(o.dir, { hook: true, noPush: !!o.noPush, format: o.format || null }); } catch (e) { process.stderr.write(` • yad capture: ${e.message}\n`); }
|
|
435
|
+
process.exitCode = 0;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Every other command. One await, once, for a process that is about to do real work anyway.
|
|
440
|
+
const commands = await import('./commands.mjs');
|
|
441
|
+
if (o.help || !cmd) {
|
|
442
|
+
const text = helpText(commands.seedableProfiles());
|
|
443
|
+
return inJSON() ? emitJSON({ help: stripAnsi(text) }) : log(text);
|
|
444
|
+
}
|
|
445
|
+
// A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
|
|
446
|
+
// shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
|
|
447
|
+
// 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());
|
|
254
449
|
|
|
255
450
|
const today = new Date().toISOString().slice(0, 10);
|
|
451
|
+
// What the command returned: under --json, a command that did not answer itself answers with it.
|
|
452
|
+
let result;
|
|
256
453
|
switch (cmd) {
|
|
257
454
|
case 'setup':
|
|
258
|
-
await runSetup(o.dir, {
|
|
455
|
+
result = await commands.runSetup(o.dir, {
|
|
259
456
|
today, force: o.force,
|
|
260
457
|
solo: o.solo, team: o.team, greenfield: o.greenfield, brownfield: o.brownfield,
|
|
261
458
|
monorepo: o.monorepo, separate: o.separate, tools: o.tools,
|
|
459
|
+
// The agent directories to install into, as a pre-answer for CI/scripts exactly like `--solo`
|
|
460
|
+
// above (E11). Until this flag existed, `ideTargets` was reachable only programmatically: a
|
|
461
|
+
// non-interactive run took `ask`'s default and had NO way to choose. That was survivable while
|
|
462
|
+
// the default was one directory; it stopped being survivable when the default became two,
|
|
463
|
+
// because a scripted setup would write a second full copy of the skills with no way to say no.
|
|
464
|
+
// `o['ide-targets']`, not `o.ideTargets`: VALUE_FLAGS strips the leading `--` and nothing else,
|
|
465
|
+
// so a hyphenated flag keeps its hyphen as the key (`--retro-ship` is read the same way below).
|
|
466
|
+
ideTargets: o['ide-targets'] === undefined ? undefined : String(o['ide-targets']).split(',').map((t) => t.trim()).filter(Boolean),
|
|
262
467
|
});
|
|
263
468
|
break;
|
|
264
469
|
case 'check':
|
|
265
|
-
await reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
470
|
+
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 });
|
|
266
471
|
break;
|
|
267
472
|
case 'update':
|
|
268
|
-
await reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
473
|
+
result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
269
474
|
break;
|
|
270
475
|
case 'doctor':
|
|
271
|
-
await runDoctor(o.dir, { json: o.json });
|
|
476
|
+
result = await commands.runDoctor(o.dir, { json: o.json });
|
|
272
477
|
break;
|
|
273
478
|
case 'migrate':
|
|
274
|
-
await runMigrate(o.dir, { apply: o.apply, json: o.json });
|
|
275
|
-
break;
|
|
276
|
-
// Harness-invoked, not typed by a human: a tool-call payload arrives on stdin and the exit code
|
|
277
|
-
// is the verdict (0 allow, 2 deny). See cli/hook.mjs for the contract.
|
|
278
|
-
case 'hook': {
|
|
279
|
-
const [, action] = o._;
|
|
280
|
-
if (action !== 'ledger-guard') {
|
|
281
|
-
log(c.red(`unknown hook: ${action ?? '(none)'} (ledger-guard)`));
|
|
282
|
-
process.exitCode = 1;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
runLedgerGuardHook({ paths: o.path ? [o.path] : [] });
|
|
479
|
+
result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json });
|
|
286
480
|
break;
|
|
287
|
-
}
|
|
288
481
|
case 'report':
|
|
289
|
-
await runReport(o.dir, { message: o.message });
|
|
482
|
+
result = await commands.runReport(o.dir, { message: o.message });
|
|
290
483
|
break;
|
|
291
484
|
case 'usage':
|
|
292
|
-
runUsage(o.dir, {
|
|
485
|
+
result = commands.runUsage(o.dir, {
|
|
293
486
|
out: o.out, since: o.since, until: o.until, all: o.all, member: o.member,
|
|
294
487
|
format: o.format, repos: o.repos, json: o.json, today,
|
|
295
488
|
});
|
|
296
489
|
break;
|
|
297
490
|
case 'sync-status': {
|
|
298
491
|
const [, epic] = o._;
|
|
299
|
-
if (epic && !isValidEpicId(epic)) {
|
|
300
|
-
await syncStatuses(o.dir, { epic, dryRun: o.dryRun });
|
|
492
|
+
if (epic && !commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
493
|
+
result = await commands.syncStatuses(o.dir, { epic, dryRun: o.dryRun });
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
case 'epic': {
|
|
497
|
+
const [, action, slug, ...extra] = o._;
|
|
498
|
+
// A stray word is refused rather than dropped: `--inherits epic architecture` takes only `epic` as
|
|
499
|
+
// the value, and silently seeding without `architecture` would carry less than the author asked.
|
|
500
|
+
if (action === 'new' && extra.length) {
|
|
501
|
+
refuse(`unexpected argument(s): ${extra.join(' ')} — a list takes commas, e.g. --inherits epic,architecture`); break;
|
|
502
|
+
}
|
|
503
|
+
if (action !== 'new') {
|
|
504
|
+
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
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
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
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
case 'foundation': {
|
|
511
|
+
const [, action] = o._;
|
|
512
|
+
if (action === 'status') { result = await commands.runFoundationStatus(o.dir, { json: o.json }); break; }
|
|
513
|
+
if (action !== 'new') {
|
|
514
|
+
refuse(`unknown foundation action: ${action ?? '(none)'} (new, status)`, 'usage: yad foundation new [--json] | yad foundation status [--json]');
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
result = await commands.runFoundationNew(o.dir, { today, json: o.json });
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
case 'skill': {
|
|
521
|
+
const [, action, step, ...rest] = o._;
|
|
522
|
+
if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json }); break; }
|
|
523
|
+
if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest }); break; }
|
|
524
|
+
if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step }); break; }
|
|
525
|
+
refuse(`unknown skill action: ${action} (list, bind, unbind)`, 'usage: yad skill list [--json] | yad skill bind <step> <skill> [<skill> ...] | yad skill unbind <step>');
|
|
301
526
|
break;
|
|
302
527
|
}
|
|
303
528
|
case 'next': {
|
|
304
529
|
const [, epic] = o._;
|
|
305
530
|
// `--check` with no step is a malformed guard call — fail loudly rather than silently print.
|
|
306
|
-
if (o.check === true) {
|
|
307
|
-
await runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all, json: o.json });
|
|
531
|
+
if (o.check === true) { refuse('usage: yad next <epic> --check <step>'); break; }
|
|
532
|
+
result = await commands.runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all, json: o.json });
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'skip':
|
|
536
|
+
case 'unskip':
|
|
537
|
+
case 'defer':
|
|
538
|
+
case 'undefer': {
|
|
539
|
+
const [verb, epic, step] = o._;
|
|
540
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid or missing epic id: ${epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
541
|
+
// A STORY id where the step goes names a whole Build lane (E39), with --repo naming the repo. No Shape
|
|
542
|
+
// step id ends in -S<n>, so the two never overlap. A lane is skipped, never deferred.
|
|
543
|
+
if (step && /-S\d+$/i.test(step)) {
|
|
544
|
+
if (verb === 'defer' || verb === 'undefer') {
|
|
545
|
+
refuse(`a Build lane is skipped whole, never deferred: yad skip ${epic} ${step} --repo <name> --reason "<why>"`); break;
|
|
546
|
+
}
|
|
547
|
+
if (o.debt) warn('--debt is not used on a Build lane: a lane is skipped whole, never owed back');
|
|
548
|
+
result = await commands.runLaneSkip(o.dir, { epic, story: step, repo: o.repo, reason: o.reason, undo: verb === 'unskip' || o.undo, today });
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
if (o.repo) warn(`--repo is only for a Build lane (yad ${verb} ${epic} <story> --repo <name>) — ignored for a Shape step`);
|
|
552
|
+
// `unskip` / `undefer` are the verbs E36 / E37 named; `--undo` is the spelling that shipped first and stays.
|
|
553
|
+
const runVerb = verb === 'defer' || verb === 'undefer' ? commands.runDefer : commands.runSkip;
|
|
554
|
+
result = await runVerb(o.dir, { epic, step, reason: o.reason, debt: o.debt, undo: verb.startsWith('un') || o.undo, today });
|
|
308
555
|
break;
|
|
309
556
|
}
|
|
310
|
-
case '
|
|
557
|
+
case 'dial': {
|
|
558
|
+
// One word names a Shape author step (project-wide). Three name a Build lane step: epic, story, step.
|
|
559
|
+
const args = o._.slice(1);
|
|
560
|
+
if (args.length === 1) {
|
|
561
|
+
result = await commands.runDial(o.dir, { step: args[0], to: o.to, json: o.json });
|
|
562
|
+
} else if (args.length === 3) {
|
|
563
|
+
if (!commands.isValidEpicId(args[0])) { refuse(`invalid epic id: ${args[0]} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
564
|
+
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
|
+
} else {
|
|
566
|
+
refuse('usage: yad dial <step> [--to auto|human] | yad dial <epic> <story> --repo <name> <step> [--to auto|human]');
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'mode': {
|
|
571
|
+
if (o._.length > 2) { refuse(`unexpected argument(s): ${o._.slice(2).join(' ')}`); break; }
|
|
572
|
+
result = await commands.runMode(o.dir, { to: o._[1] ?? null, reason: o.reason, json: o.json, today });
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case 'kill':
|
|
576
|
+
case 'unkill': {
|
|
577
|
+
if (o._.length > 1) { refuse(`unexpected argument(s): ${o._.slice(1).join(' ')}`); break; }
|
|
578
|
+
result = await commands.runKill(o.dir, { on: o._[0] === 'kill', reason: o.reason, json: o.json, today });
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case 'unblock': {
|
|
311
582
|
const [, epic, step] = o._;
|
|
312
|
-
if (!epic || !isValidEpicId(epic)) {
|
|
313
|
-
await
|
|
583
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid or missing epic id: ${epic ?? '(none)'} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
584
|
+
result = await commands.runUnblock(o.dir, { epic, step });
|
|
314
585
|
break;
|
|
315
586
|
}
|
|
316
587
|
case 'gate': {
|
|
317
588
|
const [, action, epic, artifact] = o._;
|
|
318
589
|
// `gate ci` takes no positionals — epic/artifact come from --branch (or a sweep of all PRs).
|
|
319
|
-
if (action === 'ci') { await gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
|
|
320
|
-
if (!epic) {
|
|
590
|
+
if (action === 'ci') { result = await commands.gateCi(o.dir, { branch: o.branch, pr: o.pr, merged: o.merged, push: !o.noPush, today }); break; }
|
|
591
|
+
if (!epic) { refuse('usage: yad gate <open|sync|comments|status|repair|review|walkthrough|trailer|ci|approve|comment|advance> <epic> [artifact]'); break; }
|
|
321
592
|
// The epic id becomes a path segment under epics/ — reject anything but EP-<slug> outright.
|
|
322
|
-
if (!isValidEpicId(epic)) {
|
|
593
|
+
if (!commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
323
594
|
// In verified mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
|
|
324
595
|
// advisory (reads the platform, prints status, writes nothing). The artifact status flip is
|
|
325
596
|
// CI's job at merge — never wired into the local gate. Local mode keeps local writes.
|
|
326
|
-
if (action === 'open') await gateOpen(o.dir, { epic, artifact });
|
|
327
|
-
else if (action === 'sync') await gateSync(o.dir, { epic, artifact, today, number: o.pr, local: true });
|
|
328
|
-
else if (action === 'comments') await gateComments(o.dir, { epic, artifact, today });
|
|
329
|
-
else if (action === 'status') await gateStatus(o.dir, { epic });
|
|
330
|
-
else if (action === 'repair') await gateRepair(o.dir, { epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun });
|
|
331
|
-
else if (action === 'review') await gateReview(o.dir, { epic, artifact });
|
|
332
|
-
else if (action === 'walkthrough') await gateWalkthrough(o.dir, { epic, artifact });
|
|
333
|
-
else if (action === 'trailer') await gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
|
|
334
|
-
|
|
597
|
+
if (action === 'open') result = await commands.gateOpen(o.dir, { epic, artifact, today });
|
|
598
|
+
else if (action === 'sync') result = await commands.gateSync(o.dir, { epic, artifact, today, number: o.pr, local: true });
|
|
599
|
+
else if (action === 'comments') result = await commands.gateComments(o.dir, { epic, artifact, today });
|
|
600
|
+
else if (action === 'status') result = await commands.gateStatus(o.dir, { epic });
|
|
601
|
+
else if (action === 'repair') result = await commands.gateRepair(o.dir, { epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, today });
|
|
602
|
+
else if (action === 'review') result = await commands.gateReview(o.dir, { epic, artifact });
|
|
603
|
+
else if (action === 'walkthrough') result = await commands.gateWalkthrough(o.dir, { epic, artifact });
|
|
604
|
+
else if (action === 'trailer') result = await commands.gateTrailer(o.dir, { epic, artifact, body: o.body || o.message, number: o.pr });
|
|
605
|
+
// E112 — the gate on a Product with no platform: record an approval, record a comment round, advance.
|
|
606
|
+
else if (action === 'approve') result = await commands.gateApprove(o.dir, { epic, artifact, by: o.by, engagement: o.engagement ?? null, today });
|
|
607
|
+
else if (action === 'comment') result = await commands.gateComment(o.dir, { epic, artifact, by: o.by, count: o.count ?? null, newRound: !!o.newRound, today });
|
|
608
|
+
else if (action === 'advance') result = await commands.gateAdvance(o.dir, { epic, artifact, today });
|
|
609
|
+
else { refuse(`unknown gate action: ${action} (open|sync|comments|status|repair|review|walkthrough|trailer|ci|approve|comment|advance)`); }
|
|
335
610
|
break;
|
|
336
611
|
}
|
|
337
612
|
case 'review': {
|
|
338
613
|
const [, action] = o._;
|
|
339
|
-
if (action === 'trailer') await reviewTrailer(o.dir, { repo: o.repo, pr: o.pr, body: o.body || o.message });
|
|
340
|
-
else if (action === 'context' || action === 'chat' || action === 'cards') await reviewContext(o.dir, { repo: o.repo, pr: o.pr });
|
|
341
|
-
else if (action === 'walkthrough') await reviewWalkthrough(o.dir, { repo: o.repo, pr: o.pr });
|
|
342
|
-
else if (action === 'nudge') await reviewNudge(o.dir, { repo: o.repo, pr: o.pr });
|
|
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 });
|
|
343
618
|
else if (action === 'reconcile') {
|
|
344
619
|
// The epic becomes a path segment under epics/ — reject anything but EP-<slug> (no `../` escape).
|
|
345
|
-
if (!o.epic || !isValidEpicId(o.epic)) {
|
|
346
|
-
await reviewReconcile(o.dir, { epic: o.epic, repo: o.repo, pr: o.pr });
|
|
620
|
+
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 });
|
|
347
622
|
}
|
|
348
|
-
else {
|
|
623
|
+
else { refuse('usage: yad review <trailer|context|walkthrough|nudge|reconcile> --repo <name> --pr <n> [--epic <id>] [--body <text>]'); }
|
|
349
624
|
break;
|
|
350
625
|
}
|
|
351
626
|
case 'commit':
|
|
352
|
-
await runCommit(o.dir, { type: o.type, message: o.message, task: o.task, ai: o.ai, contractChange: o.contractChange, dryRun: o.dryRun, force: o.force });
|
|
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 });
|
|
353
628
|
break;
|
|
354
629
|
case 'open-pr':
|
|
355
|
-
await 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 });
|
|
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 });
|
|
356
631
|
break;
|
|
357
632
|
case 'ship':
|
|
358
|
-
await 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 });
|
|
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 });
|
|
359
634
|
break;
|
|
360
635
|
case 'checkpoint': {
|
|
361
636
|
let retroShip;
|
|
362
637
|
if (o['retro-ship']) {
|
|
363
638
|
const [epic, story] = String(o['retro-ship']).split('/');
|
|
364
|
-
if (!epic || !isValidEpicId(epic)) {
|
|
639
|
+
if (!epic || !commands.isValidEpicId(epic)) { refuse(`invalid --retro-ship: expected <epic>/<story> with epic EP-<slug> (got ${o['retro-ship']})`); break; }
|
|
365
640
|
// The story id becomes a path element (stories/<story>.md) — pin it to the id shape (and to its
|
|
366
641
|
// own epic) so a `..` or a slash can never traverse out of the epic's stories dir, and a typo'd
|
|
367
642
|
// cross-epic id is caught here rather than failing obscurely later.
|
|
368
|
-
if (!story || !/^EP-[a-z0-9-]+-S\d+$/.test(story) || !story.startsWith(`${epic}-S`)) {
|
|
643
|
+
if (!story || !/^EP-[a-z0-9-]+-S\d+$/.test(story) || !story.startsWith(`${epic}-S`)) { refuse(`invalid --retro-ship: expected <epic>/<story> with story <epic>-S<NN> (got ${o['retro-ship']})`); break; }
|
|
369
644
|
retroShip = { epic, story, repo: o.repo, task: o.task, mergeCommit: o['merge-commit'], today };
|
|
370
645
|
}
|
|
371
|
-
await runCheckpoint(o.dir, { push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, retroShip });
|
|
646
|
+
result = await commands.runCheckpoint(o.dir, { push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun, retroShip });
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case 'capture': {
|
|
650
|
+
// E43. `--hook` is the harness's post-edit command: it never fails and never prints to stdout.
|
|
651
|
+
if (o._[1]) { refuse(`yad capture takes no word (got ${o._[1]})`, 'usage: yad capture [--no-push]'); break; }
|
|
652
|
+
result = await commands.runCapture(o.dir, { hook: !!o.hook, noPush: !!o.noPush });
|
|
653
|
+
if (o.hook) process.exitCode = 0;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case 'claims': {
|
|
657
|
+
// E46. Read from the capture branches; nothing is recorded here.
|
|
658
|
+
const [, epic, extra] = o._;
|
|
659
|
+
if (extra) { refuse(`yad claims takes at most an epic (got an extra word: ${extra})`, 'usage: yad claims [<epic>] [--no-fetch]'); break; }
|
|
660
|
+
result = await commands.runClaims(o.dir, { epic, noFetch: !!o.noFetch });
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case 'assign':
|
|
664
|
+
case 'unassign': {
|
|
665
|
+
// E47. A file per step; nothing is committed and nothing is blocked.
|
|
666
|
+
const [verb, epic, step, extra] = o._;
|
|
667
|
+
const usage = verb === 'assign' ? 'usage: yad assign <epic> <step> [--to <git name>] [--force]' : 'usage: yad unassign <epic> <step> [--force]';
|
|
668
|
+
if (extra) { refuse(`yad ${verb} takes an epic and a step (got an extra word: ${extra})`, usage); break; }
|
|
669
|
+
if (verb === 'unassign' && o.to != null) { refuse('yad unassign takes no --to: it removes whoever is assigned', usage); break; }
|
|
670
|
+
result = verb === 'assign'
|
|
671
|
+
? await commands.runAssign(o.dir, { epic, step, to: o.to ?? null, force: !!o.force, today })
|
|
672
|
+
: await commands.runUnassign(o.dir, { epic, step, force: !!o.force });
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case 'owners': {
|
|
676
|
+
const [, epic, extra] = o._;
|
|
677
|
+
if (extra) { refuse(`yad owners takes at most an epic (got an extra word: ${extra})`, 'usage: yad owners [<epic>]'); break; }
|
|
678
|
+
result = await commands.runOwners(o.dir, { epic });
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 'fold': {
|
|
682
|
+
// E44. The step-boundary commit; the authoring skills run it where they used to say "commit".
|
|
683
|
+
const [, epic, step, extra] = o._;
|
|
684
|
+
if (extra) { refuse(`yad fold takes an epic and a step (got an extra word: ${extra})`, 'usage: yad fold <epic> <step>'); break; }
|
|
685
|
+
result = await commands.runFold(o.dir, { epic, step });
|
|
372
686
|
break;
|
|
373
687
|
}
|
|
374
688
|
case 'tidy': {
|
|
375
689
|
const [, action, epic] = o._;
|
|
376
|
-
if (action !== 'up') {
|
|
377
|
-
|
|
690
|
+
if (action !== 'up') {
|
|
691
|
+
// A bare `yad tidy` is a request for the usage, not a mistake: exit 0.
|
|
692
|
+
if (action) refuse(`unknown tidy action: ${action} (up)`, 'usage: yad tidy up [<epic>] [--push] [--dry-run]');
|
|
693
|
+
else if (inJSON()) emitJSON({ usage: 'yad tidy up [<epic>] [--push] [--dry-run]' });
|
|
694
|
+
else log(`usage: yad tidy up [<epic>] [--push] [--dry-run]`);
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
result = await commands.runTidy(o.dir, { epic: epic || o.epic, push: o.push, allowBranch: o.allowBranch, dryRun: o.dryRun });
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case 'index':
|
|
701
|
+
result = await commands.runIndex(o.dir, { json: o.json });
|
|
702
|
+
break;
|
|
703
|
+
case 'history': {
|
|
704
|
+
const [, action, ...args] = o._;
|
|
705
|
+
// Every flag that is not history's own is refused, not ignored: an ignored `--since` would read as
|
|
706
|
+
// a filter that was applied. Read from the RAW arguments, as typed — `parseArgs` turns a flag it
|
|
707
|
+
// does not know (`--limit`, a typo) into a plain word, which `search` would take as text, and it
|
|
708
|
+
// stores `--preview` under another name. A word that starts with `--`, or a one-letter flag such
|
|
709
|
+
// as `-m`, is a flag; a value (`--type chore`) never starts with `-`, because `parseArgs` refuses it.
|
|
710
|
+
const HISTORY_OWN = new Set(commands.HISTORY_FLAGS);
|
|
711
|
+
const unknownFlags = [...new Set(process.argv.slice(2)
|
|
712
|
+
.filter((t) => /^--./.test(t) || /^-[A-Za-z]$/.test(t))
|
|
713
|
+
.map((t) => t.split('=')[0])
|
|
714
|
+
.filter((f) => !HISTORY_OWN.has(f)))];
|
|
715
|
+
result = await commands.runHistory(o.dir, {
|
|
716
|
+
action: action || 'list', args, json: !!o.json, unknownFlags,
|
|
717
|
+
type: o.type ?? null, theme: o.theme ?? null, thread: o.thread ?? null, open: !!o.open, done: !!o.done,
|
|
718
|
+
});
|
|
378
719
|
break;
|
|
379
720
|
}
|
|
380
721
|
case 'repo': {
|
|
381
722
|
const [, action, name] = o._;
|
|
382
|
-
await runRepo(o.dir, { action: action || 'list', name, today, push: o.push, allowBranch: o.allowBranch });
|
|
723
|
+
result = await commands.runRepo(o.dir, { action: action || 'list', name, today, push: o.push, allowBranch: o.allowBranch });
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case 'risk-map': {
|
|
727
|
+
const [, action, name] = o._;
|
|
728
|
+
result = await commands.runRiskMap(o.dir, { action: action || 'check', name, json: o.json, dryRun: o.dryRun });
|
|
383
729
|
break;
|
|
384
730
|
}
|
|
385
|
-
case '
|
|
386
|
-
const [, action,
|
|
387
|
-
|
|
731
|
+
case 'codeowners': {
|
|
732
|
+
const [, action, name] = o._;
|
|
733
|
+
// `--write` was dropped (E69); wherever it is typed, it is refused with the reason — never ignored.
|
|
734
|
+
result = await commands.runCodeowners(o.dir, { action: action || 'check', name, json: o.json, platform: o.platform, write: o._.some((a) => a === '--write' || a.startsWith('--write=')) });
|
|
388
735
|
break;
|
|
389
736
|
}
|
|
737
|
+
case 'roster':
|
|
738
|
+
// Removed in E62. Kept as a word for one major so a script or a habit gets told where it went
|
|
739
|
+
// instead of "unknown command".
|
|
740
|
+
refuse('yad roster was removed — yadflow keeps no list of people',
|
|
741
|
+
'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: '
|
|
743
|
+
+ 'it decides nothing, but the first sync uses its name → login pairs to recognise those approvals.');
|
|
744
|
+
break;
|
|
390
745
|
case 'docs': {
|
|
391
746
|
const [, action] = o._;
|
|
392
|
-
if (o.epic && !isValidEpicId(o.epic)) {
|
|
747
|
+
if (o.epic && !commands.isValidEpicId(o.epic)) { refuse(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
393
748
|
const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
|
|
394
|
-
await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
749
|
+
result = await commands.runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
395
750
|
break;
|
|
396
751
|
}
|
|
397
752
|
case 'thread': {
|
|
398
753
|
const [, epic] = o._;
|
|
399
|
-
if (epic && !isValidEpicId(epic)) {
|
|
400
|
-
await runThread(o.dir, { epic, json: o.json });
|
|
754
|
+
if (epic && !commands.isValidEpicId(epic)) { refuse(`invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
755
|
+
result = await commands.runThread(o.dir, { epic, json: o.json });
|
|
401
756
|
break;
|
|
402
757
|
}
|
|
403
758
|
case 'reconcile': {
|
|
404
759
|
const [, action] = o._;
|
|
405
760
|
const thread = o.epic || o.thread || null;
|
|
406
|
-
if (thread && !isValidEpicId(thread)) {
|
|
761
|
+
if (thread && !commands.isValidEpicId(thread)) { refuse(`invalid epic id: ${thread} (expected EP-<slug>, [a-z0-9-] only)`); break; }
|
|
407
762
|
const act = action || (o.wire ? 'wire' : o.refresh ? 'refresh' : 'check');
|
|
408
763
|
if (!['check', 'refresh', 'wire'].includes(act)) {
|
|
409
|
-
|
|
764
|
+
refuse(`unknown reconcile action: ${act} (check|refresh|wire)`); break;
|
|
410
765
|
}
|
|
411
|
-
await runReconcile(o.dir, { action: act, thread });
|
|
766
|
+
result = await commands.runReconcile(o.dir, { action: act, thread });
|
|
412
767
|
break;
|
|
413
768
|
}
|
|
414
769
|
default:
|
|
770
|
+
if (inJSON()) { refuse(`unknown command: ${cmd}`, '`yad --help` lists the commands'); break; }
|
|
415
771
|
log(c.red(`unknown command: ${cmd}`));
|
|
416
|
-
log(
|
|
772
|
+
log(helpText(commands.seedableProfiles()));
|
|
417
773
|
process.exitCode = 1;
|
|
418
774
|
}
|
|
775
|
+
return result;
|
|
419
776
|
}
|
|
420
777
|
|
|
421
778
|
main()
|
|
779
|
+
// A command that finished a --json run without answering is a yadflow bug. Stdout still gets one
|
|
780
|
+
// object saying so, never an empty stream a parser chokes on — and never a guessed `ok: true`.
|
|
781
|
+
.then((result) => {
|
|
782
|
+
if (!inJSON() || jsonEmitted()) return;
|
|
783
|
+
// A refusal said in prose (`fail` + its `hint`) becomes the JSON refusal.
|
|
784
|
+
// What the command DID rides along: a push that failed after the commit landed must not read as
|
|
785
|
+
// "nothing happened" (E1 review) — `ship` answers `committed: true` beside the refusal.
|
|
786
|
+
const said = process.exitCode ? jsonFailure() : null;
|
|
787
|
+
if (said) {
|
|
788
|
+
// Never a key the envelope or the refusal owns: a clash would throw inside the answer and lose the
|
|
789
|
+
// real refusal, and a `json` key is an option of `refuse`, not data.
|
|
790
|
+
const owned = new Set([...ENVELOPE_KEYS, 'ok', 'error', 'code', 'hint', 'warnings', 'json']);
|
|
791
|
+
const did = Object.fromEntries(Object.entries(isPlainObject(result) ? result : {}).filter(([k]) => !owned.has(k)));
|
|
792
|
+
return refuse(said.error, said.hint, { code: said.code, ...did });
|
|
793
|
+
}
|
|
794
|
+
// Otherwise the command's result object IS the answer — `ok` follows the exit code, as everywhere.
|
|
795
|
+
if (isPlainObject(result)) return emitJSON({ ...result, ok: !process.exitCode });
|
|
796
|
+
refuse(`yad ${runningCmd ?? ''} gave no JSON answer — this is a yadflow bug`.replace(' ', ' '), 'run it without --json to see what it printed, and report it with `yad report`');
|
|
797
|
+
})
|
|
422
798
|
.catch(async (err) => {
|
|
799
|
+
// Under --json every failure is still one object on stdout (E1) — a refusal from the parser, a
|
|
800
|
+
// thrown YadError, a bug. The `code` is the `YAD-` code README "Troubleshooting" is keyed on.
|
|
801
|
+
// If the command had already answered, stdout holds its object, and the failure goes to stderr.
|
|
802
|
+
if (inJSON() && !jsonEmitted()) {
|
|
803
|
+
if (err?.parsedCmd !== undefined && runningCmd === null) beginJSON(commandName(err.parsedWords ?? []));
|
|
804
|
+
const hint = err?.hint || (/expects a value$/.test(String(err?.message)) ? '`yad --help` lists the flags of each command' : null);
|
|
805
|
+
refuse(String(err?.message || err), hint, { code: err?.code && /^YAD-/.test(err.code) ? err.code : null });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
423
808
|
const code = err?.code && /^YAD-/.test(err.code) ? ` [${err.code}]` : '';
|
|
424
809
|
log(c.red(`\nyad failed${code}: ${err?.message || err}`));
|
|
425
810
|
if (err?.hint) log(c.yellow(` → ${err.hint}`));
|
|
@@ -429,11 +814,12 @@ main()
|
|
|
429
814
|
// YAD_NO_REPORT. `report` failing on its own would land here, so never re-offer for it.
|
|
430
815
|
// Require a TTY on BOTH ends: stdout for the message, stdin so the y/N prompt can be answered
|
|
431
816
|
// (a TTY stdout with piped/closed stdin would otherwise hang on readline).
|
|
432
|
-
const offerReport = process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
|
|
817
|
+
const offerReport = !inJSON() && process.stdin.isTTY && process.stdout.isTTY && !process.env.SDLC_NONINTERACTIVE
|
|
433
818
|
&& !process.env.YAD_NO_REPORT && process.argv[2] !== 'report';
|
|
434
819
|
if (offerReport) {
|
|
435
820
|
try {
|
|
436
821
|
if (await askYesNo('\nReport this failure to the yadflow team?', false)) {
|
|
822
|
+
const { runReport } = await import('./commands.mjs');
|
|
437
823
|
await runReport(process.cwd(), { error: err });
|
|
438
824
|
}
|
|
439
825
|
} catch { /* reporting is best-effort — never mask the original failure */ }
|
|
@@ -450,7 +836,10 @@ main()
|
|
|
450
836
|
// block reason travels on — an update banner there would land in front of a model.
|
|
451
837
|
// Resolved the way main() resolves it, NOT from argv[2]: that is the first raw argument, so
|
|
452
838
|
// `yad --dir <path> hook ledger-guard` puts `--dir` there and the banner slips through.
|
|
453
|
-
if (parseArgs(process.argv.slice(2))._[0] !== 'hook')
|
|
839
|
+
if (parseArgs(process.argv.slice(2))._[0] !== 'hook') {
|
|
840
|
+
const { maybeNotifyUpdate } = await import('./commands.mjs');
|
|
841
|
+
await maybeNotifyUpdate();
|
|
842
|
+
}
|
|
454
843
|
} catch { /* the notice is never worth failing or hanging a command over */ } finally {
|
|
455
844
|
closePrompts();
|
|
456
845
|
}
|