@yagni-app/code 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +34 -0
- package/dist/extension/rerouteNotice.js +79 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI init-pass idempotency marker (Onramp Door B, spec §5B/§7).
|
|
3
|
+
*
|
|
4
|
+
* The init pass writes only to `yagni_code_decisions`, but freshness is computed
|
|
5
|
+
* from `GET /api/yagni-code/context` — which reads only Vision + active Goals.
|
|
6
|
+
* A workspace with neither (the common case for a fresh CLI-only user) therefore
|
|
7
|
+
* looks "fresh" on EVERY launch, so without a marker the init pass would re-run
|
|
8
|
+
* and accumulate duplicate decisions each time.
|
|
9
|
+
*
|
|
10
|
+
* The fix is a one-time, per-workspace marker file, dropped after the first real
|
|
11
|
+
* run. It lives in the same `~/.yagni-code` home as the run journal
|
|
12
|
+
* ({@link checkpoint.ts}), is keyed on a truncated SHA-256 of the workspace id so
|
|
13
|
+
* the filename is path-safe and reveals no identity, and holds only a timestamp
|
|
14
|
+
* (never a token or any PII). Every operation is fail-soft: a missing marker
|
|
15
|
+
* simply means "not yet run", and a write failure never breaks session start.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { codeStateHome } from "./stateHome.js";
|
|
21
|
+
/**
|
|
22
|
+
* Test seam mirroring {@link checkpoint._setCheckpointHomeForTest}: when set, the
|
|
23
|
+
* marker roots here instead of `~/.yagni-code`, so tests hit a tmpdir. Pass
|
|
24
|
+
* `null` to restore.
|
|
25
|
+
*/
|
|
26
|
+
let homeOverride = null;
|
|
27
|
+
export function _setInitDoneHomeForTest(dir) {
|
|
28
|
+
homeOverride = dir;
|
|
29
|
+
}
|
|
30
|
+
function initDoneHome() {
|
|
31
|
+
return codeStateHome(homeOverride);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The marker path for a workspace. The id is hashed (SHA-256, first 16 hex chars)
|
|
35
|
+
* so an arbitrary/opaque workspace id is path-safe and the raw id never lands in
|
|
36
|
+
* a filename.
|
|
37
|
+
*/
|
|
38
|
+
export function initDoneMarkerFile(workspaceId) {
|
|
39
|
+
const hash = createHash("sha256").update(workspaceId).digest("hex").slice(0, 16);
|
|
40
|
+
return join(initDoneHome(), `init-done-${hash}`);
|
|
41
|
+
}
|
|
42
|
+
/** Has the init pass already run once for this workspace? Fail-soft → false. */
|
|
43
|
+
export function isInitDone(workspaceId) {
|
|
44
|
+
if (!workspaceId)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
return existsSync(initDoneMarkerFile(workspaceId));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false; // unreadable → treat as not-yet-run (the caller still guards on freshness)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Record that the init pass has run once for this workspace. Fail-soft (never throws). */
|
|
54
|
+
export function markInitDone(workspaceId) {
|
|
55
|
+
if (!workspaceId)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(initDoneHome(), { recursive: true, mode: 0o700 });
|
|
59
|
+
// Content is a bare timestamp — no token, no PII (the id is hashed in the name).
|
|
60
|
+
writeFileSync(initDoneMarkerFile(workspaceId), `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* fail-soft: a marker write must never break session start */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=initDone.js.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI first-run experience (Onramp Door B, spec §5B/§7, as amended by
|
|
3
|
+
* ADR-0033: first run is a welcome, not a Team ceremony).
|
|
4
|
+
*
|
|
5
|
+
* On a FRESH workspace (an empty/thin grounding corpus, via the existing
|
|
6
|
+
* `GET /api/yagni-code/context` brief), the first interactive `yagni` run shows
|
|
7
|
+
* a short WELCOME: how to work with YAGNI in one notice, then one offer (ask
|
|
8
|
+
* @yagni what to work on next) with free text as a first-class choice.
|
|
9
|
+
* Escape/cancel land in an empty editor ready to type into, never a forced
|
|
10
|
+
* prompt. The welcome records NOTHING and reads nothing from the repo.
|
|
11
|
+
*
|
|
12
|
+
* The Team-drafting flow (repo intake -> engineering-half draft -> approve/edit
|
|
13
|
+
* -> bank the brief as ONE decision) lives behind the explicit `/setup-team`
|
|
14
|
+
* command ({@link registerTeamSetupCommand}) so onboarding can invoke it
|
|
15
|
+
* deliberately. Honesty rails are unchanged (spec §9, F2/F13): approval-gated
|
|
16
|
+
* writes, honest-when-thin (nothing fabricated), the Team is only ever a DRAFT
|
|
17
|
+
* and never created automatically, and the repo intake reads only universal
|
|
18
|
+
* signals (README, AGENTS.md/CLAUDE.md, package.json scripts) so it behaves
|
|
19
|
+
* the same on any customer repo.
|
|
20
|
+
*
|
|
21
|
+
* No new backend transport: the only writes are through the existing token-scoped
|
|
22
|
+
* `record_decision` endpoint (reused via {@link recordDecision}). Every seam is
|
|
23
|
+
* injectable so both flows are unit-testable without a network or a filesystem.
|
|
24
|
+
*/
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { fetchContextBrief as defaultFetchContextBrief, type ContextBrief } from "./config.js";
|
|
27
|
+
import { type NextActionOption } from "./nextWorkTool.js";
|
|
28
|
+
import { recordDecision as defaultRecordDecision, type RecordDecisionParams } from "./recordDecisionTool.js";
|
|
29
|
+
/**
|
|
30
|
+
* Below this many non-whitespace characters (and with no sources), a returned
|
|
31
|
+
* context brief is treated as thin/empty — i.e. the corpus has not been seeded
|
|
32
|
+
* yet, so this is a fresh workspace that should run the init pass.
|
|
33
|
+
*/
|
|
34
|
+
export declare const FRESH_BRIEF_MIN_CHARS = 40;
|
|
35
|
+
/**
|
|
36
|
+
* Is this a fresh workspace (an empty/thin grounding corpus)? A `null` brief, or
|
|
37
|
+
* a brief with only a stub of text and no sources, means nothing has grounded the
|
|
38
|
+
* workspace yet. A brief with real text OR any sources means it is already
|
|
39
|
+
* populated and the init pass must be skipped.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isFreshWorkspace(brief: ContextBrief | null | undefined): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* What the repo intake could find. All fields are best-effort / optional, and
|
|
44
|
+
* every signal is a UNIVERSAL repo convention (README, agents file, package
|
|
45
|
+
* scripts) — never a house-specific layout like docs/adr, so the intake
|
|
46
|
+
* behaves the same on any customer repo.
|
|
47
|
+
*/
|
|
48
|
+
export interface RepoIntake {
|
|
49
|
+
/** README body (capped), when present. */
|
|
50
|
+
readme?: string;
|
|
51
|
+
/** AGENTS.md or CLAUDE.md body (capped), when present. */
|
|
52
|
+
agents?: string;
|
|
53
|
+
/** package.json `scripts` map (empty when absent/unparseable). */
|
|
54
|
+
packageScripts: Record<string, string>;
|
|
55
|
+
/** The literal test command (package.json `scripts.test`), when present. */
|
|
56
|
+
testCommand?: string;
|
|
57
|
+
/** The literal build command (package.json `scripts.build`), when present. */
|
|
58
|
+
buildCommand?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Injectable filesystem seam so repo intake is unit-testable without disk. */
|
|
61
|
+
export interface RepoIntakeFs {
|
|
62
|
+
readFile(p: string): Promise<string>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the repo at `cwd` for the signals that seed the engineering half of the
|
|
66
|
+
* brief. Every read is best-effort; a missing file simply drops that signal.
|
|
67
|
+
*/
|
|
68
|
+
export declare function readRepoIntake(cwd: string, fs?: RepoIntakeFs): Promise<RepoIntake>;
|
|
69
|
+
/** A proposed Engineering Team — a DRAFT that is never auto-committed. */
|
|
70
|
+
export interface DraftTeam {
|
|
71
|
+
name: string;
|
|
72
|
+
mission: string;
|
|
73
|
+
responsibilities: string[];
|
|
74
|
+
playbookRules: string[];
|
|
75
|
+
testCommand?: string;
|
|
76
|
+
buildCommand?: string;
|
|
77
|
+
}
|
|
78
|
+
/** The drafted engineering half: the brief, seeded decisions, and a Team draft. */
|
|
79
|
+
export interface EngineeringDraft {
|
|
80
|
+
/** The engineering-half brief text (honest-when-thin). */
|
|
81
|
+
brief: string;
|
|
82
|
+
/** True when the repo had no README/AGENTS — the brief says so, nothing is fabricated. */
|
|
83
|
+
thin: boolean;
|
|
84
|
+
/** Decisions to seed into the corpus. EMPTY when thin (no fabrication). */
|
|
85
|
+
decisions: RecordDecisionParams[];
|
|
86
|
+
/** The proposed Engineering Team (a draft; never auto-committed). */
|
|
87
|
+
team: DraftTeam;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Draft the engineering half of the brief from repo intake. PURE. Honest-when-thin:
|
|
91
|
+
* with no README/AGENTS the brief says so and NO decision is seeded.
|
|
92
|
+
*/
|
|
93
|
+
export declare function draftEngineering(intake: RepoIntake): EngineeringDraft;
|
|
94
|
+
/** Injectable dependencies for {@link runInitPass} (the first-run welcome). */
|
|
95
|
+
export interface RunInitPassDeps {
|
|
96
|
+
baseUrl: string;
|
|
97
|
+
getToken: () => string | undefined;
|
|
98
|
+
fetchImpl?: typeof fetch;
|
|
99
|
+
/**
|
|
100
|
+
* A pre-fetched context brief (the caller usually already fetched it to boot
|
|
101
|
+
* grounded). When `undefined`, the pass fetches it itself. Pass `null`
|
|
102
|
+
* explicitly to signal "fetched, and it was empty".
|
|
103
|
+
*/
|
|
104
|
+
brief?: ContextBrief | null;
|
|
105
|
+
fetchContextBrief?: typeof defaultFetchContextBrief;
|
|
106
|
+
}
|
|
107
|
+
/** The outcome of a first-run welcome attempt. The welcome never writes. */
|
|
108
|
+
export type InitPassOutcome = {
|
|
109
|
+
ran: false;
|
|
110
|
+
reason: "not_fresh" | "non_interactive";
|
|
111
|
+
} | {
|
|
112
|
+
ran: true;
|
|
113
|
+
/** What the user picked; `free_text` covers the typing option AND escape/cancel. */
|
|
114
|
+
chosenAction: NextActionOption["id"] | "free_text";
|
|
115
|
+
};
|
|
116
|
+
/** Injectable dependencies for {@link runTeamSetup} (the `/setup-team` flow). */
|
|
117
|
+
export interface RunTeamSetupDeps {
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
getToken: () => string | undefined;
|
|
120
|
+
fetchImpl?: typeof fetch;
|
|
121
|
+
readRepoIntake?: (cwd: string) => Promise<RepoIntake>;
|
|
122
|
+
recordDecision?: typeof defaultRecordDecision;
|
|
123
|
+
}
|
|
124
|
+
/** The outcome of a `/setup-team` run. */
|
|
125
|
+
export type TeamSetupOutcome = {
|
|
126
|
+
ran: false;
|
|
127
|
+
reason: "non_interactive";
|
|
128
|
+
} | {
|
|
129
|
+
ran: true;
|
|
130
|
+
thin: boolean;
|
|
131
|
+
decisionsRecorded: number;
|
|
132
|
+
/** The Team is NEVER created/committed automatically. Always false. */
|
|
133
|
+
teamCommitted: false;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Compact one-screen PREVIEW of the draft for the approve/edit dialog.
|
|
137
|
+
*
|
|
138
|
+
* Approval-gated (spec §5B/§9, F2/F13): NOTHING is written to the corpus until
|
|
139
|
+
* the human approves or corrects the draft, so this is a preview of INTENT — it
|
|
140
|
+
* states what will be recorded ON APPROVAL, and never claims a corpus was
|
|
141
|
+
* already seeded. When thin there is nothing to record (honest — nothing to
|
|
142
|
+
* fabricate). The ACTUAL recorded count is reported after the write runs (see
|
|
143
|
+
* `runInitPass`).
|
|
144
|
+
*/
|
|
145
|
+
export declare function summarizeDraft(draft: EngineeringDraft): string;
|
|
146
|
+
/**
|
|
147
|
+
* Run the first-run welcome (ADR-0033). Guards on fresh-workspace detection
|
|
148
|
+
* (defensive — the caller also guards), shows a short how-to-work-with-YAGNI
|
|
149
|
+
* notice, and offers ONE default next action (ask @yagni what to work on next)
|
|
150
|
+
* with free text as a first-class choice. Picking "Just start typing",
|
|
151
|
+
* pressing escape, or cancelling all leave the editor untouched so the user
|
|
152
|
+
* can type whatever they want to start on. Records NOTHING, reads nothing from
|
|
153
|
+
* the repo, and is fully fail-soft (it must never break session start).
|
|
154
|
+
*/
|
|
155
|
+
export declare function runInitPass(pi: ExtensionAPI, ctx: ExtensionContext, deps: RunInitPassDeps): Promise<InitPassOutcome>;
|
|
156
|
+
/**
|
|
157
|
+
* Run the `/setup-team` flow: read the repo's universal signals, draft the
|
|
158
|
+
* engineering half + a proposed Team, present it for approve/edit, and ONLY
|
|
159
|
+
* THEN bank the brief as one decision (approval-gated: an approve banks the
|
|
160
|
+
* draft, an edit banks the correction, a decline writes nothing). Fully
|
|
161
|
+
* fail-soft; the Team itself is never created automatically.
|
|
162
|
+
*/
|
|
163
|
+
export declare function runTeamSetup(pi: ExtensionAPI, ctx: ExtensionContext, deps: RunTeamSetupDeps): Promise<TeamSetupOutcome>;
|
|
164
|
+
/**
|
|
165
|
+
* Register `/setup-team`: the explicit, approval-gated flow that drafts the
|
|
166
|
+
* Engineering Team + engineering brief from the repo. Deliberately NOT part of
|
|
167
|
+
* first-run (ADR-0033) — onboarding invokes it when the workspace is ready.
|
|
168
|
+
*/
|
|
169
|
+
export declare function registerTeamSetupCommand(pi: ExtensionAPI, opts: RunTeamSetupDeps): void;
|
|
170
|
+
//# sourceMappingURL=initPass.d.ts.map
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI first-run experience (Onramp Door B, spec §5B/§7, as amended by
|
|
3
|
+
* ADR-0033: first run is a welcome, not a Team ceremony).
|
|
4
|
+
*
|
|
5
|
+
* On a FRESH workspace (an empty/thin grounding corpus, via the existing
|
|
6
|
+
* `GET /api/yagni-code/context` brief), the first interactive `yagni` run shows
|
|
7
|
+
* a short WELCOME: how to work with YAGNI in one notice, then one offer (ask
|
|
8
|
+
* @yagni what to work on next) with free text as a first-class choice.
|
|
9
|
+
* Escape/cancel land in an empty editor ready to type into, never a forced
|
|
10
|
+
* prompt. The welcome records NOTHING and reads nothing from the repo.
|
|
11
|
+
*
|
|
12
|
+
* The Team-drafting flow (repo intake -> engineering-half draft -> approve/edit
|
|
13
|
+
* -> bank the brief as ONE decision) lives behind the explicit `/setup-team`
|
|
14
|
+
* command ({@link registerTeamSetupCommand}) so onboarding can invoke it
|
|
15
|
+
* deliberately. Honesty rails are unchanged (spec §9, F2/F13): approval-gated
|
|
16
|
+
* writes, honest-when-thin (nothing fabricated), the Team is only ever a DRAFT
|
|
17
|
+
* and never created automatically, and the repo intake reads only universal
|
|
18
|
+
* signals (README, AGENTS.md/CLAUDE.md, package.json scripts) so it behaves
|
|
19
|
+
* the same on any customer repo.
|
|
20
|
+
*
|
|
21
|
+
* No new backend transport: the only writes are through the existing token-scoped
|
|
22
|
+
* `record_decision` endpoint (reused via {@link recordDecision}). Every seam is
|
|
23
|
+
* injectable so both flows are unit-testable without a network or a filesystem.
|
|
24
|
+
*/
|
|
25
|
+
import { readFile as fsReadFile } from "node:fs/promises";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fetchContextBrief as defaultFetchContextBrief, } from "./config.js";
|
|
28
|
+
import { defaultNextAction } from "./nextWorkTool.js";
|
|
29
|
+
import { recordDecision as defaultRecordDecision, } from "./recordDecisionTool.js";
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Fresh-workspace detection
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* Below this many non-whitespace characters (and with no sources), a returned
|
|
35
|
+
* context brief is treated as thin/empty — i.e. the corpus has not been seeded
|
|
36
|
+
* yet, so this is a fresh workspace that should run the init pass.
|
|
37
|
+
*/
|
|
38
|
+
export const FRESH_BRIEF_MIN_CHARS = 40;
|
|
39
|
+
/**
|
|
40
|
+
* Is this a fresh workspace (an empty/thin grounding corpus)? A `null` brief, or
|
|
41
|
+
* a brief with only a stub of text and no sources, means nothing has grounded the
|
|
42
|
+
* workspace yet. A brief with real text OR any sources means it is already
|
|
43
|
+
* populated and the init pass must be skipped.
|
|
44
|
+
*/
|
|
45
|
+
export function isFreshWorkspace(brief) {
|
|
46
|
+
if (!brief)
|
|
47
|
+
return true;
|
|
48
|
+
const text = (brief.brief ?? "").trim();
|
|
49
|
+
const hasSources = Array.isArray(brief.sources) && brief.sources.length > 0;
|
|
50
|
+
return text.length < FRESH_BRIEF_MIN_CHARS && !hasSources;
|
|
51
|
+
}
|
|
52
|
+
/** The default fs seam: node:fs/promises, resolving to empty on any error. */
|
|
53
|
+
const nodeIntakeFs = {
|
|
54
|
+
async readFile(p) {
|
|
55
|
+
try {
|
|
56
|
+
return await fsReadFile(p, "utf8");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
/** Candidate filenames, tried in order; the first that reads non-empty wins. */
|
|
64
|
+
const README_CANDIDATES = ["README.md", "README", "readme.md", "README.markdown"];
|
|
65
|
+
const AGENTS_CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
|
|
66
|
+
/** Cap the amount of prose we lift so a large README never bloats the brief. */
|
|
67
|
+
const MAX_DOC_CHARS = 4000;
|
|
68
|
+
const MAX_SUMMARY_CHARS = 280;
|
|
69
|
+
async function firstNonEmpty(fs, cwd, names) {
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const body = (await fs.readFile(path.join(cwd, name))).trim();
|
|
72
|
+
if (body)
|
|
73
|
+
return body.slice(0, MAX_DOC_CHARS);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Parse the `scripts` map out of a package.json body; {} on any problem. */
|
|
78
|
+
function parseScripts(raw) {
|
|
79
|
+
if (!raw.trim())
|
|
80
|
+
return {};
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(raw);
|
|
83
|
+
const scripts = parsed.scripts;
|
|
84
|
+
if (!scripts || typeof scripts !== "object")
|
|
85
|
+
return {};
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [k, v] of Object.entries(scripts)) {
|
|
88
|
+
if (typeof v === "string")
|
|
89
|
+
out[k] = v;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read the repo at `cwd` for the signals that seed the engineering half of the
|
|
99
|
+
* brief. Every read is best-effort; a missing file simply drops that signal.
|
|
100
|
+
*/
|
|
101
|
+
export async function readRepoIntake(cwd, fs = nodeIntakeFs) {
|
|
102
|
+
const [readme, agents, pkgRaw] = await Promise.all([
|
|
103
|
+
firstNonEmpty(fs, cwd, README_CANDIDATES),
|
|
104
|
+
firstNonEmpty(fs, cwd, AGENTS_CANDIDATES),
|
|
105
|
+
fs.readFile(path.join(cwd, "package.json")),
|
|
106
|
+
]);
|
|
107
|
+
const packageScripts = parseScripts(pkgRaw);
|
|
108
|
+
return {
|
|
109
|
+
readme,
|
|
110
|
+
agents,
|
|
111
|
+
packageScripts,
|
|
112
|
+
testCommand: packageScripts.test || undefined,
|
|
113
|
+
buildCommand: packageScripts.build || undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const THIN_BRIEF = "I couldn't find a README or AGENTS.md/CLAUDE.md in this repo, so I don't " +
|
|
117
|
+
"know much about how you build yet. Tell me in a line what this repo is and how " +
|
|
118
|
+
"you build it, and I'll fill in the engineering brief.";
|
|
119
|
+
const TEAM_MISSION = "Owns how we build: conventions, reviews, and test/build.";
|
|
120
|
+
/** Cap `text` at `max` chars, cutting back to the last word boundary with an ellipsis. */
|
|
121
|
+
function truncateAtWord(text, max) {
|
|
122
|
+
if (text.length <= max)
|
|
123
|
+
return text;
|
|
124
|
+
const cut = text.slice(0, max);
|
|
125
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
126
|
+
return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
|
|
127
|
+
}
|
|
128
|
+
/** The README's first real paragraph (heading-stripped), capped at a word boundary. */
|
|
129
|
+
function firstParagraph(readme) {
|
|
130
|
+
if (!readme)
|
|
131
|
+
return undefined;
|
|
132
|
+
const para = readme
|
|
133
|
+
.split(/\n\s*\n/)
|
|
134
|
+
.map((p) => p.trim())
|
|
135
|
+
.find((p) => p && !p.startsWith("#") && !/^[!\[]/.test(p));
|
|
136
|
+
if (!para)
|
|
137
|
+
return undefined;
|
|
138
|
+
return truncateAtWord(para.replace(/\s+/g, " "), MAX_SUMMARY_CHARS);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Draft the engineering half of the brief from repo intake. PURE. Honest-when-thin:
|
|
142
|
+
* with no README/AGENTS the brief says so and NO decision is seeded.
|
|
143
|
+
*/
|
|
144
|
+
export function draftEngineering(intake) {
|
|
145
|
+
const hasSubstance = Boolean(intake.readme || intake.agents);
|
|
146
|
+
const thin = !hasSubstance;
|
|
147
|
+
const team = {
|
|
148
|
+
name: "Engineering",
|
|
149
|
+
mission: TEAM_MISSION,
|
|
150
|
+
responsibilities: [],
|
|
151
|
+
playbookRules: [],
|
|
152
|
+
testCommand: intake.testCommand,
|
|
153
|
+
buildCommand: intake.buildCommand,
|
|
154
|
+
};
|
|
155
|
+
if (intake.testCommand)
|
|
156
|
+
team.playbookRules.push(`Run \`${intake.testCommand}\` before handing off a change.`);
|
|
157
|
+
if (intake.buildCommand)
|
|
158
|
+
team.playbookRules.push(`Run \`${intake.buildCommand}\` to verify the change builds.`);
|
|
159
|
+
team.playbookRules.push("Ask @yagni before guessing on anything org- or codebase-specific; record product-intent decisions as you make them.");
|
|
160
|
+
if (thin) {
|
|
161
|
+
team.responsibilities.push("Define how this team builds software (not documented yet).");
|
|
162
|
+
return { brief: THIN_BRIEF, thin: true, decisions: [], team };
|
|
163
|
+
}
|
|
164
|
+
if (intake.agents)
|
|
165
|
+
team.responsibilities.push("Uphold the conventions documented in AGENTS.md/CLAUDE.md.");
|
|
166
|
+
if (intake.testCommand || intake.buildCommand)
|
|
167
|
+
team.responsibilities.push("Keep the build and tests green.");
|
|
168
|
+
if (team.responsibilities.length === 0)
|
|
169
|
+
team.responsibilities.push("Own how this team builds software.");
|
|
170
|
+
const lines = ["How this team builds software (drafted from repo intake — a draft, editable):"];
|
|
171
|
+
const whatItIs = firstParagraph(intake.readme);
|
|
172
|
+
if (whatItIs)
|
|
173
|
+
lines.push(`- What this is: ${whatItIs}`);
|
|
174
|
+
if (intake.agents)
|
|
175
|
+
lines.push("- Conventions: documented in AGENTS.md/CLAUDE.md.");
|
|
176
|
+
if (intake.testCommand)
|
|
177
|
+
lines.push(`- Tests: \`${intake.testCommand}\``);
|
|
178
|
+
if (intake.buildCommand)
|
|
179
|
+
lines.push(`- Build: \`${intake.buildCommand}\``);
|
|
180
|
+
const brief = lines.join("\n");
|
|
181
|
+
// Seed the corpus with ONE entry so the very first /go has real ground to
|
|
182
|
+
// cite: the drafted brief itself. Repo docs stay in the repo where the agent
|
|
183
|
+
// can read them whole; regex-lifted fragments are never banked as decisions.
|
|
184
|
+
// The brief is a DRAFT (editable), never treated as immutable fact.
|
|
185
|
+
const decisions = [
|
|
186
|
+
{
|
|
187
|
+
question: "How does this team build software?",
|
|
188
|
+
decision: brief,
|
|
189
|
+
rationale: "Drafted from repo intake (README/AGENTS) during the first YAGNI Code run; a draft, editable.",
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
return { brief, thin: false, decisions, team };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compact one-screen PREVIEW of the draft for the approve/edit dialog.
|
|
196
|
+
*
|
|
197
|
+
* Approval-gated (spec §5B/§9, F2/F13): NOTHING is written to the corpus until
|
|
198
|
+
* the human approves or corrects the draft, so this is a preview of INTENT — it
|
|
199
|
+
* states what will be recorded ON APPROVAL, and never claims a corpus was
|
|
200
|
+
* already seeded. When thin there is nothing to record (honest — nothing to
|
|
201
|
+
* fabricate). The ACTUAL recorded count is reported after the write runs (see
|
|
202
|
+
* `runInitPass`).
|
|
203
|
+
*/
|
|
204
|
+
export function summarizeDraft(draft) {
|
|
205
|
+
const parts = [draft.brief, "", `Proposed Team: ${draft.team.name} — ${draft.team.mission}`];
|
|
206
|
+
if (draft.team.responsibilities.length) {
|
|
207
|
+
parts.push("Responsibilities:", ...draft.team.responsibilities.map((r) => ` - ${r}`));
|
|
208
|
+
}
|
|
209
|
+
if (draft.thin) {
|
|
210
|
+
parts.push("", "(Engineering context is thin — nothing to record yet; I never fabricate one.)");
|
|
211
|
+
}
|
|
212
|
+
else if (draft.decisions.length > 0) {
|
|
213
|
+
parts.push("", "On approval I'll save this brief into your grounding corpus. Nothing else is recorded.");
|
|
214
|
+
}
|
|
215
|
+
parts.push("", "This Team is a draft — it is never created automatically. You can edit it first.");
|
|
216
|
+
return parts.join("\n");
|
|
217
|
+
}
|
|
218
|
+
/** A permissive notify wrapper: a notice must never disrupt a flow. */
|
|
219
|
+
function safeNotify(ui) {
|
|
220
|
+
return (message, type) => {
|
|
221
|
+
try {
|
|
222
|
+
ui?.notify?.(message, type);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* swallowed */
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** The one-notice welcome shown on the very first interactive run in a repo. */
|
|
230
|
+
const WELCOME_NOTICE = "First run in this repo. Just type to start working. Ask me about your company " +
|
|
231
|
+
"context (tickets, docs, decisions), or run /go <ticket> for a ticket-to-PR run.";
|
|
232
|
+
/** The free-text choice: an untouched editor, ready for whatever they want to build. */
|
|
233
|
+
const FREE_TEXT_LABEL = "Just start typing";
|
|
234
|
+
/**
|
|
235
|
+
* Run the first-run welcome (ADR-0033). Guards on fresh-workspace detection
|
|
236
|
+
* (defensive — the caller also guards), shows a short how-to-work-with-YAGNI
|
|
237
|
+
* notice, and offers ONE default next action (ask @yagni what to work on next)
|
|
238
|
+
* with free text as a first-class choice. Picking "Just start typing",
|
|
239
|
+
* pressing escape, or cancelling all leave the editor untouched so the user
|
|
240
|
+
* can type whatever they want to start on. Records NOTHING, reads nothing from
|
|
241
|
+
* the repo, and is fully fail-soft (it must never break session start).
|
|
242
|
+
*/
|
|
243
|
+
export async function runInitPass(pi, ctx, deps) {
|
|
244
|
+
// 1. Fresh-workspace guard (skip when already grounded).
|
|
245
|
+
const brief = deps.brief !== undefined
|
|
246
|
+
? deps.brief
|
|
247
|
+
: await (deps.fetchContextBrief ?? defaultFetchContextBrief)({
|
|
248
|
+
baseUrl: deps.baseUrl,
|
|
249
|
+
getToken: deps.getToken,
|
|
250
|
+
fetchImpl: deps.fetchImpl,
|
|
251
|
+
}).catch(() => null);
|
|
252
|
+
if (!isFreshWorkspace(brief))
|
|
253
|
+
return { ran: false, reason: "not_fresh" };
|
|
254
|
+
// In non-interactive / print (`-p`) / no-UI mode there is no one to welcome;
|
|
255
|
+
// skip WITHOUT marking init-done so the first interactive run still gets it.
|
|
256
|
+
const ui = (ctx.hasUI ? ctx.ui : undefined);
|
|
257
|
+
if (!ui)
|
|
258
|
+
return { ran: false, reason: "non_interactive" };
|
|
259
|
+
const notify = safeNotify(ui);
|
|
260
|
+
notify(WELCOME_NOTICE, "info");
|
|
261
|
+
// 2. Offer the ONE default next action (spec §7.3: defaults over choices),
|
|
262
|
+
// with free text as a first-class door. Everything here is best-effort:
|
|
263
|
+
// whatever happens, the user ends at a prompt they can just type into.
|
|
264
|
+
let chosenAction = "free_text";
|
|
265
|
+
try {
|
|
266
|
+
const next = defaultNextAction();
|
|
267
|
+
const review = next.escapeHatches.find((h) => h.id === "review");
|
|
268
|
+
if (typeof ui.select === "function") {
|
|
269
|
+
const options = [next.primary, ...(review ? [review] : [])];
|
|
270
|
+
const labels = [next.primary.label, FREE_TEXT_LABEL, ...(review ? [review.label] : [])];
|
|
271
|
+
const picked = await ui.select("What next?", labels);
|
|
272
|
+
const chosen = options.find((o) => o.label === picked);
|
|
273
|
+
// "Just start typing", escape, and cancel all leave the editor alone —
|
|
274
|
+
// a first run must never force a prompt on the user.
|
|
275
|
+
if (chosen) {
|
|
276
|
+
chosenAction = chosen.id;
|
|
277
|
+
ui.setEditorText?.(chosen.prompt);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
/* the offer is best-effort; free text always works */
|
|
283
|
+
}
|
|
284
|
+
return { ran: true, chosenAction };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Run the `/setup-team` flow: read the repo's universal signals, draft the
|
|
288
|
+
* engineering half + a proposed Team, present it for approve/edit, and ONLY
|
|
289
|
+
* THEN bank the brief as one decision (approval-gated: an approve banks the
|
|
290
|
+
* draft, an edit banks the correction, a decline writes nothing). Fully
|
|
291
|
+
* fail-soft; the Team itself is never created automatically.
|
|
292
|
+
*/
|
|
293
|
+
export async function runTeamSetup(pi, ctx, deps) {
|
|
294
|
+
const recordDecisionFn = deps.recordDecision ?? defaultRecordDecision;
|
|
295
|
+
const readIntake = deps.readRepoIntake ?? ((cwd) => readRepoIntake(cwd));
|
|
296
|
+
const decisionOpts = {
|
|
297
|
+
baseUrl: deps.baseUrl,
|
|
298
|
+
getToken: deps.getToken,
|
|
299
|
+
fetchImpl: deps.fetchImpl,
|
|
300
|
+
};
|
|
301
|
+
// Approval-gated writes (spec §5B/§9 — captured-judgment beat): a decision
|
|
302
|
+
// only earns its place when a human can see and approve/edit the draft. With
|
|
303
|
+
// no UI there is no one to approve, so we NEVER write.
|
|
304
|
+
const ui = (ctx.hasUI ? ctx.ui : undefined);
|
|
305
|
+
if (!ui)
|
|
306
|
+
return { ran: false, reason: "non_interactive" };
|
|
307
|
+
const notify = safeNotify(ui);
|
|
308
|
+
notify("Reading the repo to draft your Engineering Team…", "info");
|
|
309
|
+
// 1. Read the repo + draft the engineering half.
|
|
310
|
+
let intake;
|
|
311
|
+
try {
|
|
312
|
+
intake = await readIntake(ctx.cwd ?? process.cwd());
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
intake = { packageScripts: {} };
|
|
316
|
+
}
|
|
317
|
+
const draft = draftEngineering(intake);
|
|
318
|
+
// 2. Present the draft for approve/edit BEFORE any durable write (F2/F13).
|
|
319
|
+
// In a UI without a `confirm` capability there is no way to obtain
|
|
320
|
+
// approval, so (consistent with the no-UI guard above) we write nothing.
|
|
321
|
+
let decisionsRecorded = 0;
|
|
322
|
+
try {
|
|
323
|
+
let approved = false;
|
|
324
|
+
let correctedBrief;
|
|
325
|
+
if (typeof ui.confirm === "function") {
|
|
326
|
+
approved = await ui.confirm("Your draft Engineering Team", summarizeDraft(draft));
|
|
327
|
+
if (!approved && typeof ui.editor === "function") {
|
|
328
|
+
const edited = await ui.editor("Edit the engineering brief", draft.brief);
|
|
329
|
+
if (edited && edited.trim() && edited.trim() !== draft.brief.trim()) {
|
|
330
|
+
correctedBrief = edited.trim();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// 3. Approval-gated corpus writes. APPROVE → bank the drafted brief
|
|
335
|
+
// decision. REJECT-then-correct → bank ONLY the correction (the
|
|
336
|
+
// rejected auto-draft is never written). REJECT without an edit (or no
|
|
337
|
+
// approval UI) → write NOTHING. Best-effort throughout: a failed write
|
|
338
|
+
// is swallowed and the recorded count stays honest (F13).
|
|
339
|
+
if (approved) {
|
|
340
|
+
for (const decision of draft.decisions) {
|
|
341
|
+
try {
|
|
342
|
+
const result = await recordDecisionFn(decisionOpts, decision, ctx.signal);
|
|
343
|
+
if (!result.spooled)
|
|
344
|
+
decisionsRecorded += 1;
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* fail-soft: recording is best-effort */
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (correctedBrief) {
|
|
352
|
+
// The edit is a captured-judgment beat: bank the correction so the next
|
|
353
|
+
// run inherits the steer.
|
|
354
|
+
try {
|
|
355
|
+
const result = await recordDecisionFn(decisionOpts, {
|
|
356
|
+
question: "How does this team build software?",
|
|
357
|
+
decision: correctedBrief,
|
|
358
|
+
rationale: "The user edited the drafted engineering brief during team setup.",
|
|
359
|
+
}, ctx.signal);
|
|
360
|
+
if (!result.spooled)
|
|
361
|
+
decisionsRecorded += 1;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
/* fail-soft */
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Report the ACTUAL recorded count once the write has run (never the
|
|
368
|
+
// intended count) so the receipt is honest even on a partial/failed write.
|
|
369
|
+
if (decisionsRecorded > 0) {
|
|
370
|
+
notify("Saved your engineering brief into the grounding corpus.", "info");
|
|
371
|
+
}
|
|
372
|
+
else if (!draft.thin) {
|
|
373
|
+
notify("Nothing recorded.", "info");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
/* the presentation is best-effort */
|
|
378
|
+
}
|
|
379
|
+
return { ran: true, thin: draft.thin, decisionsRecorded, teamCommitted: false };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Register `/setup-team`: the explicit, approval-gated flow that drafts the
|
|
383
|
+
* Engineering Team + engineering brief from the repo. Deliberately NOT part of
|
|
384
|
+
* first-run (ADR-0033) — onboarding invokes it when the workspace is ready.
|
|
385
|
+
*/
|
|
386
|
+
export function registerTeamSetupCommand(pi, opts) {
|
|
387
|
+
pi.registerCommand("setup-team", {
|
|
388
|
+
description: "Draft an Engineering Team + engineering brief from this repo; nothing is saved without your approval.",
|
|
389
|
+
handler: async (_args, ctx) => {
|
|
390
|
+
await runTeamSetup(pi, ctx, opts);
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=initPass.js.map
|