work-kit-cli 0.2.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -13
- package/cli/src/commands/bootstrap.test.ts +40 -0
- package/cli/src/commands/bootstrap.ts +77 -13
- package/cli/src/commands/cancel.ts +1 -16
- package/cli/src/commands/complete.ts +92 -98
- package/cli/src/commands/completions.ts +2 -2
- package/cli/src/commands/doctor.ts +1 -1
- package/cli/src/commands/extract.ts +217 -0
- package/cli/src/commands/init.test.ts +50 -0
- package/cli/src/commands/init.ts +70 -35
- package/cli/src/commands/learn.test.ts +217 -0
- package/cli/src/commands/learn.ts +104 -0
- package/cli/src/commands/loopback.ts +8 -11
- package/cli/src/commands/next.ts +93 -60
- package/cli/src/commands/observe.ts +16 -21
- package/cli/src/commands/pause-resume.test.ts +142 -0
- package/cli/src/commands/pause.ts +34 -0
- package/cli/src/commands/report.ts +217 -0
- package/cli/src/commands/resume.ts +126 -0
- package/cli/src/commands/setup.ts +280 -0
- package/cli/src/commands/status.ts +8 -6
- package/cli/src/commands/uninstall.ts +8 -3
- package/cli/src/commands/workflow.ts +43 -33
- package/cli/src/config/agent-map.ts +9 -9
- package/cli/src/config/constants.ts +54 -0
- package/cli/src/config/loopback-routes.ts +13 -13
- package/cli/src/config/model-routing.test.ts +190 -0
- package/cli/src/config/model-routing.ts +208 -0
- package/cli/src/config/project-config.test.ts +127 -0
- package/cli/src/config/project-config.ts +106 -0
- package/cli/src/config/{phases.ts → workflow.ts} +40 -23
- package/cli/src/context/prompt-builder.ts +10 -9
- package/cli/src/index.ts +130 -9
- package/cli/src/observer/data.ts +196 -65
- package/cli/src/observer/renderer.ts +127 -107
- package/cli/src/observer/watcher.ts +28 -16
- package/cli/src/state/helpers.test.ts +28 -28
- package/cli/src/state/helpers.ts +37 -25
- package/cli/src/state/schema.ts +135 -45
- package/cli/src/state/store.ts +127 -7
- package/cli/src/state/validators.test.ts +13 -13
- package/cli/src/state/validators.ts +3 -4
- package/cli/src/utils/colors.ts +2 -0
- package/cli/src/utils/fs.ts +13 -0
- package/cli/src/utils/json.ts +20 -0
- package/cli/src/utils/knowledge.ts +471 -0
- package/cli/src/utils/time.ts +27 -0
- package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
- package/cli/src/workflow/loopbacks.ts +42 -0
- package/cli/src/workflow/parallel.ts +64 -0
- package/cli/src/workflow/transitions.test.ts +129 -0
- package/cli/src/{engine → workflow}/transitions.ts +18 -22
- package/package.json +2 -2
- package/skills/auto-kit/SKILL.md +44 -27
- package/skills/cancel-kit/SKILL.md +4 -4
- package/skills/full-kit/SKILL.md +45 -28
- package/skills/pause-kit/SKILL.md +25 -0
- package/skills/resume-kit/SKILL.md +64 -0
- package/skills/wk-bootstrap/SKILL.md +11 -5
- package/skills/wk-build/SKILL.md +12 -11
- package/skills/wk-build/{stages → steps}/commit.md +1 -1
- package/skills/wk-build/{stages → steps}/core.md +3 -3
- package/skills/wk-build/{stages → steps}/integration.md +2 -2
- package/skills/wk-build/{stages → steps}/migration.md +1 -1
- package/skills/wk-build/{stages → steps}/red.md +1 -1
- package/skills/wk-build/{stages → steps}/refactor.md +1 -1
- package/skills/wk-build/{stages → steps}/setup.md +1 -1
- package/skills/wk-build/{stages → steps}/ui.md +1 -1
- package/skills/wk-deploy/SKILL.md +7 -6
- package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
- package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
- package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
- package/skills/wk-plan/SKILL.md +15 -14
- package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
- package/skills/wk-plan/{stages → steps}/audit.md +2 -2
- package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
- package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
- package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
- package/skills/wk-plan/{stages → steps}/scope.md +1 -1
- package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
- package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
- package/skills/wk-review/SKILL.md +11 -10
- package/skills/wk-review/{stages → steps}/compliance.md +1 -1
- package/skills/wk-review/{stages → steps}/handoff.md +2 -2
- package/skills/wk-review/{stages → steps}/performance.md +1 -1
- package/skills/wk-review/{stages → steps}/security.md +1 -1
- package/skills/wk-review/{stages → steps}/self-review.md +1 -1
- package/skills/wk-test/SKILL.md +9 -8
- package/skills/wk-test/steps/e2e.md +56 -0
- package/skills/wk-test/{stages → steps}/validate.md +1 -1
- package/skills/wk-test/{stages → steps}/verify.md +1 -1
- package/skills/wk-wrap-up/SKILL.md +19 -5
- package/skills/wk-wrap-up/steps/knowledge.md +76 -0
- package/skills/wk-wrap-up/steps/summary.md +86 -0
- package/cli/src/engine/loopbacks.ts +0 -32
- package/cli/src/engine/parallel.ts +0 -60
- package/cli/src/engine/transitions.test.ts +0 -129
- package/skills/wk-test/stages/e2e.md +0 -53
- /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { findWorktreeRoot, readState, readStateMd, resolveMainRepoRoot, gitHeadSha } from "../state/store.js";
|
|
2
|
+
import {
|
|
3
|
+
appendAutoEntries,
|
|
4
|
+
ensureKnowledgeDir,
|
|
5
|
+
fileForType,
|
|
6
|
+
isKnowledgeType,
|
|
7
|
+
KNOWLEDGE_TYPES,
|
|
8
|
+
redact,
|
|
9
|
+
type KnowledgeEntry,
|
|
10
|
+
type KnowledgeType,
|
|
11
|
+
} from "../utils/knowledge.js";
|
|
12
|
+
import { skillFilePath } from "../config/workflow.js";
|
|
13
|
+
import type { PhaseName, WorkKitState } from "../state/schema.js";
|
|
14
|
+
|
|
15
|
+
export interface ExtractOptions {
|
|
16
|
+
worktreeRoot?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExtractResult {
|
|
20
|
+
action: "extracted" | "error";
|
|
21
|
+
written: number;
|
|
22
|
+
duplicates: number;
|
|
23
|
+
byType: Record<KnowledgeType, number>;
|
|
24
|
+
message?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RawEntry {
|
|
28
|
+
type: KnowledgeType;
|
|
29
|
+
text: string;
|
|
30
|
+
phase?: string;
|
|
31
|
+
step?: string;
|
|
32
|
+
source: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function emptyByType(): Record<KnowledgeType, number> {
|
|
36
|
+
const out = {} as Record<KnowledgeType, number>;
|
|
37
|
+
for (const t of KNOWLEDGE_TYPES) out[t] = 0;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Single-pass state.md parser ─────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const OBSERVATION_RE = /^-\s*\[([a-z]+)(?::([a-z0-9-]+\/[a-z0-9-]+))?\]\s*(.+)$/i;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Walk state.md once and emit raw entries from the three sections we know:
|
|
47
|
+
* Observations (typed bullets), Decisions (any bullet → convention),
|
|
48
|
+
* Deviations (any bullet → workflow with [deviation] prefix).
|
|
49
|
+
*/
|
|
50
|
+
function parseStateMd(stateMd: string): RawEntry[] {
|
|
51
|
+
const out: RawEntry[] = [];
|
|
52
|
+
if (!stateMd) return out;
|
|
53
|
+
|
|
54
|
+
let section: "observations" | "decisions" | "deviations" | null = null;
|
|
55
|
+
|
|
56
|
+
for (const rawLine of stateMd.split("\n")) {
|
|
57
|
+
const trimmed = rawLine.trim();
|
|
58
|
+
|
|
59
|
+
if (trimmed.startsWith("## ")) {
|
|
60
|
+
const header = trimmed.slice(3).trim().toLowerCase();
|
|
61
|
+
if (header === "observations") section = "observations";
|
|
62
|
+
else if (header === "decisions") section = "decisions";
|
|
63
|
+
else if (header === "deviations") section = "deviations";
|
|
64
|
+
else section = null;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (section === null) continue;
|
|
69
|
+
if (!trimmed.startsWith("-") || trimmed.startsWith("<!--")) continue;
|
|
70
|
+
|
|
71
|
+
if (section === "observations") {
|
|
72
|
+
const m = trimmed.match(OBSERVATION_RE);
|
|
73
|
+
if (!m) continue;
|
|
74
|
+
const tag = m[1].toLowerCase();
|
|
75
|
+
if (!isKnowledgeType(tag)) continue;
|
|
76
|
+
const phaseStep = m[2];
|
|
77
|
+
const text = m[3].trim();
|
|
78
|
+
if (text.length === 0) continue;
|
|
79
|
+
const entry: RawEntry = { type: tag, text, source: "auto-state-md" };
|
|
80
|
+
if (phaseStep) {
|
|
81
|
+
const [p, s] = phaseStep.split("/");
|
|
82
|
+
entry.phase = p;
|
|
83
|
+
entry.step = s;
|
|
84
|
+
}
|
|
85
|
+
out.push(entry);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const text = trimmed.replace(/^-\s*/, "").trim();
|
|
90
|
+
if (text.length === 0) continue;
|
|
91
|
+
|
|
92
|
+
if (section === "decisions") {
|
|
93
|
+
out.push({ type: "convention", text, source: "auto-state-md" });
|
|
94
|
+
} else if (section === "deviations") {
|
|
95
|
+
out.push({ type: "workflow", text: `[deviation] ${text}`, source: "auto-state-md" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Tracker.json extraction ─────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function fromLoopbacks(state: WorkKitState): RawEntry[] {
|
|
105
|
+
return (state.loopbacks ?? []).map((lb) => ({
|
|
106
|
+
type: "workflow" as const,
|
|
107
|
+
text: `[loopback] ${lb.from.phase}/${lb.from.step} → ${lb.to.phase}/${lb.to.step}: ${lb.reason}`,
|
|
108
|
+
phase: lb.from.phase,
|
|
109
|
+
step: lb.from.step,
|
|
110
|
+
source: "auto-tracker",
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fromSkippedAndFailed(state: WorkKitState): RawEntry[] {
|
|
115
|
+
const out: RawEntry[] = [];
|
|
116
|
+
for (const [phaseName, phaseState] of Object.entries(state.phases)) {
|
|
117
|
+
for (const [stepName, stepState] of Object.entries(phaseState.steps)) {
|
|
118
|
+
if (stepState.status === "skipped") {
|
|
119
|
+
out.push({
|
|
120
|
+
type: "workflow",
|
|
121
|
+
text: `[skipped] ${phaseName}/${stepName} was skipped during this session.`,
|
|
122
|
+
phase: phaseName,
|
|
123
|
+
step: stepName,
|
|
124
|
+
source: "auto-tracker",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (stepState.outcome === "broken" || stepState.outcome === "fix_needed") {
|
|
128
|
+
out.push({
|
|
129
|
+
type: "workflow",
|
|
130
|
+
text: `[failure] ${phaseName}/${stepName} reported outcome=${stepState.outcome}.`,
|
|
131
|
+
phase: phaseName,
|
|
132
|
+
step: stepName,
|
|
133
|
+
source: "auto-tracker",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export function extractCommand(opts: ExtractOptions = {}): ExtractResult {
|
|
144
|
+
const root = opts.worktreeRoot || findWorktreeRoot();
|
|
145
|
+
if (!root) {
|
|
146
|
+
return {
|
|
147
|
+
action: "error",
|
|
148
|
+
written: 0,
|
|
149
|
+
duplicates: 0,
|
|
150
|
+
byType: emptyByType(),
|
|
151
|
+
message: "No work-kit session found. Run from inside a worktree.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let state: WorkKitState;
|
|
156
|
+
try {
|
|
157
|
+
state = readState(root);
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
return {
|
|
160
|
+
action: "error",
|
|
161
|
+
written: 0,
|
|
162
|
+
duplicates: 0,
|
|
163
|
+
byType: emptyByType(),
|
|
164
|
+
message: `Could not read state: ${e.message}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mainRepoRoot = state.metadata?.mainRepoRoot ?? resolveMainRepoRoot(root);
|
|
169
|
+
ensureKnowledgeDir(mainRepoRoot);
|
|
170
|
+
|
|
171
|
+
const stateMd = readStateMd(root) ?? "";
|
|
172
|
+
|
|
173
|
+
const raw: RawEntry[] = [
|
|
174
|
+
...parseStateMd(stateMd),
|
|
175
|
+
...fromLoopbacks(state),
|
|
176
|
+
...fromSkippedAndFailed(state),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const ts = new Date().toISOString();
|
|
180
|
+
const sha = gitHeadSha(mainRepoRoot);
|
|
181
|
+
|
|
182
|
+
// Group entries by destination file for a single read-modify-write per file.
|
|
183
|
+
const grouped = new Map<string, KnowledgeEntry[]>();
|
|
184
|
+
for (const r of raw) {
|
|
185
|
+
const phase = r.phase ?? state.currentPhase ?? undefined;
|
|
186
|
+
const step = r.step ?? state.currentStep ?? undefined;
|
|
187
|
+
const skillPath = phase && step ? skillFilePath(phase as PhaseName, step) : undefined;
|
|
188
|
+
const { text } = redact(r.text);
|
|
189
|
+
|
|
190
|
+
const entry: KnowledgeEntry = {
|
|
191
|
+
ts,
|
|
192
|
+
sessionSlug: state.slug,
|
|
193
|
+
phase,
|
|
194
|
+
step,
|
|
195
|
+
skillPath,
|
|
196
|
+
gitSha: sha,
|
|
197
|
+
source: r.source,
|
|
198
|
+
text,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const file = fileForType(r.type);
|
|
202
|
+
const bucket = grouped.get(file);
|
|
203
|
+
if (bucket) bucket.push(entry);
|
|
204
|
+
else grouped.set(file, [entry]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = appendAutoEntries(mainRepoRoot, grouped);
|
|
208
|
+
|
|
209
|
+
// Map per-file write counts back to per-type counts. Each KnowledgeType
|
|
210
|
+
// routes to exactly one file, so the lookup is unambiguous.
|
|
211
|
+
const byType = emptyByType();
|
|
212
|
+
for (const t of KNOWLEDGE_TYPES) {
|
|
213
|
+
byType[t] = result.perFile.get(fileForType(t))?.written ?? 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { action: "extracted", written: result.written, duplicates: result.duplicates, byType };
|
|
217
|
+
}
|
|
@@ -94,6 +94,56 @@ describe("initCommand", () => {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it("persists model policy when provided", () => {
|
|
98
|
+
const tmp = makeTmpDir();
|
|
99
|
+
tmpDirs.push(tmp);
|
|
100
|
+
|
|
101
|
+
initCommand({
|
|
102
|
+
mode: "full",
|
|
103
|
+
description: "Ship the avatar feature",
|
|
104
|
+
modelPolicy: "opus",
|
|
105
|
+
worktreeRoot: tmp,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const state = JSON.parse(
|
|
109
|
+
fs.readFileSync(path.join(tmp, ".work-kit", "tracker.json"), "utf-8")
|
|
110
|
+
);
|
|
111
|
+
assert.equal(state.modelPolicy, "opus");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("omits modelPolicy from state when defaulting to auto", () => {
|
|
115
|
+
const tmp = makeTmpDir();
|
|
116
|
+
tmpDirs.push(tmp);
|
|
117
|
+
|
|
118
|
+
initCommand({
|
|
119
|
+
mode: "full",
|
|
120
|
+
description: "Some default task",
|
|
121
|
+
worktreeRoot: tmp,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const state = JSON.parse(
|
|
125
|
+
fs.readFileSync(path.join(tmp, ".work-kit", "tracker.json"), "utf-8")
|
|
126
|
+
);
|
|
127
|
+
assert.equal(state.modelPolicy, undefined);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects invalid model policy", () => {
|
|
131
|
+
const tmp = makeTmpDir();
|
|
132
|
+
tmpDirs.push(tmp);
|
|
133
|
+
|
|
134
|
+
const result = initCommand({
|
|
135
|
+
mode: "full",
|
|
136
|
+
description: "Task with bad policy",
|
|
137
|
+
modelPolicy: "turbo" as any,
|
|
138
|
+
worktreeRoot: tmp,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
assert.equal(result.action, "error");
|
|
142
|
+
if (result.action === "error") {
|
|
143
|
+
assert.ok(result.message.includes("model-policy"));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
97
147
|
it("auto mode with classification succeeds", () => {
|
|
98
148
|
const tmp = makeTmpDir();
|
|
99
149
|
tmpDirs.push(tmp);
|
package/cli/src/commands/init.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES,
|
|
4
|
-
import { writeState, writeStateMd, stateExists } from "../state/store.js";
|
|
5
|
-
import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/
|
|
3
|
+
import { WorkKitState, PhaseState, PhaseName, PHASE_NAMES, STEPS_BY_PHASE, WorkflowStep, Classification, MODE_FULL, MODE_AUTO, ModelPolicy, isModelPolicy } from "../state/schema.js";
|
|
4
|
+
import { writeState, writeStateMd, stateExists, STATE_DIR, resolveMainRepoRoot } from "../state/store.js";
|
|
5
|
+
import { buildFullWorkflow, buildDefaultWorkflow, skillFilePath } from "../config/workflow.js";
|
|
6
|
+
import { BRANCH_PREFIX, CLI_BINARY } from "../config/constants.js";
|
|
7
|
+
import { loadProjectConfig } from "../config/project-config.js";
|
|
8
|
+
import { resolveModel } from "../config/model-routing.js";
|
|
6
9
|
import type { Action } from "../state/schema.js";
|
|
7
10
|
|
|
8
11
|
function toSlug(description: string): string {
|
|
@@ -18,23 +21,23 @@ function buildPhases(workflow?: WorkflowStep[]): Record<PhaseName, PhaseState> {
|
|
|
18
21
|
const phases = {} as Record<PhaseName, PhaseState>;
|
|
19
22
|
|
|
20
23
|
for (const phase of PHASE_NAMES) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
24
|
+
const steps: Record<string, { status: "pending" | "skipped" }> = {};
|
|
25
|
+
const allSteps = STEPS_BY_PHASE[phase];
|
|
23
26
|
|
|
24
|
-
for (const
|
|
27
|
+
for (const s of allSteps) {
|
|
25
28
|
if (workflow) {
|
|
26
|
-
const
|
|
27
|
-
|
|
29
|
+
const ws = workflow.find((w) => w.phase === phase && w.step === s);
|
|
30
|
+
steps[s] = { status: ws?.included ? "pending" : "skipped" };
|
|
28
31
|
} else {
|
|
29
|
-
|
|
32
|
+
steps[s] = { status: "pending" };
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Check if entire phase is skipped (all
|
|
34
|
-
const allSkipped = Object.values(
|
|
36
|
+
// Check if entire phase is skipped (all steps skipped)
|
|
37
|
+
const allSkipped = Object.values(steps).every((s) => s.status === "skipped");
|
|
35
38
|
phases[phase] = {
|
|
36
39
|
status: allSkipped ? "skipped" : "pending",
|
|
37
|
-
|
|
40
|
+
steps,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -58,7 +61,7 @@ function generateStateMd(slug: string, branch: string, mode: string, description
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
md += `**Phase:** plan
|
|
61
|
-
**
|
|
64
|
+
**Step:** clarify
|
|
62
65
|
**Status:** in-progress
|
|
63
66
|
|
|
64
67
|
## Description
|
|
@@ -67,9 +70,9 @@ ${description}
|
|
|
67
70
|
|
|
68
71
|
if (workflow) {
|
|
69
72
|
md += `\n## Workflow\n`;
|
|
70
|
-
for (const
|
|
71
|
-
if (
|
|
72
|
-
const label = `${
|
|
73
|
+
for (const ws of workflow) {
|
|
74
|
+
if (ws.included) {
|
|
75
|
+
const label = `${ws.phase.charAt(0).toUpperCase() + ws.phase.slice(1)}: ${ws.step.charAt(0).toUpperCase() + ws.step.slice(1)}`;
|
|
73
76
|
md += `- [ ] ${label}\n`;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -83,6 +86,15 @@ ${description}
|
|
|
83
86
|
<!-- Append here whenever you choose between real alternatives -->
|
|
84
87
|
<!-- Format: **<context>**: chose <X> over <Y> — <why> -->
|
|
85
88
|
|
|
89
|
+
## Observations
|
|
90
|
+
<!-- Append typed bullets as you notice things worth preserving across sessions. -->
|
|
91
|
+
<!-- wrap-up/knowledge routes these to .work-kit-knowledge/. -->
|
|
92
|
+
<!-- Grammar: - [lesson|convention|risk|workflow] text (workflow tag may include :phase/step) -->
|
|
93
|
+
<!-- Examples: -->
|
|
94
|
+
<!-- - [risk] auth.middleware.ts breaks if SESSION_SECRET is unset. -->
|
|
95
|
+
<!-- - [convention] All API errors must use createApiError() helper. -->
|
|
96
|
+
<!-- - [workflow:test/e2e] The e2e step doesn't tell agents to start the dev server first. -->
|
|
97
|
+
|
|
86
98
|
## Deviations
|
|
87
99
|
<!-- Append here whenever implementation diverges from the Blueprint -->
|
|
88
100
|
<!-- Format: **<Blueprint step>**: <what changed> — <why> -->
|
|
@@ -91,9 +103,12 @@ ${description}
|
|
|
91
103
|
return md;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Append `entry` to `<root>/.gitignore` if it isn't already present.
|
|
108
|
+
* Idempotent. Creates the file if missing. Reused by setup.ts.
|
|
109
|
+
*/
|
|
110
|
+
export function ensureGitignored(root: string, entry: string): void {
|
|
111
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
97
112
|
|
|
98
113
|
if (fs.existsSync(gitignorePath)) {
|
|
99
114
|
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
@@ -105,14 +120,30 @@ function ensureGitignored(worktreeRoot: string): void {
|
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
export function initCommand(options: {
|
|
108
|
-
mode
|
|
123
|
+
mode?: "full" | "auto";
|
|
109
124
|
description: string;
|
|
110
125
|
classification?: Classification;
|
|
111
126
|
gated?: boolean;
|
|
127
|
+
modelPolicy?: ModelPolicy;
|
|
112
128
|
worktreeRoot?: string;
|
|
113
129
|
}): Action {
|
|
114
|
-
const { mode, description, classification, gated } = options;
|
|
115
130
|
const worktreeRoot = options.worktreeRoot || process.cwd();
|
|
131
|
+
const mainRepoRoot = resolveMainRepoRoot(worktreeRoot);
|
|
132
|
+
const projectConfig = loadProjectConfig(mainRepoRoot);
|
|
133
|
+
|
|
134
|
+
const mode = options.mode ?? projectConfig.defaults?.mode ?? "full";
|
|
135
|
+
const classification = options.classification ?? projectConfig.defaults?.classification;
|
|
136
|
+
const gated = options.gated ?? projectConfig.defaults?.gated ?? false;
|
|
137
|
+
const modelPolicy: ModelPolicy = options.modelPolicy ?? "auto";
|
|
138
|
+
const { description } = options;
|
|
139
|
+
|
|
140
|
+
// Validate model policy (guards against CLI callers that bypass the commander layer)
|
|
141
|
+
if (!isModelPolicy(modelPolicy)) {
|
|
142
|
+
return {
|
|
143
|
+
action: "error",
|
|
144
|
+
message: `Invalid --model-policy "${modelPolicy}". Use one of: auto, opus, sonnet, haiku, inherit.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
116
147
|
|
|
117
148
|
// Guard: don't overwrite existing state
|
|
118
149
|
if (stateExists(worktreeRoot)) {
|
|
@@ -140,63 +171,67 @@ export function initCommand(options: {
|
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
const slug = toSlug(description);
|
|
143
|
-
const branch =
|
|
144
|
-
const modeLabel = mode === "full" ?
|
|
174
|
+
const branch = `${BRANCH_PREFIX}${slug}`;
|
|
175
|
+
const modeLabel = mode === "full" ? MODE_FULL : MODE_AUTO;
|
|
145
176
|
|
|
146
177
|
// Build workflow
|
|
147
178
|
let workflow: WorkflowStep[] | undefined;
|
|
148
179
|
if (mode === "auto" && classification) {
|
|
149
|
-
workflow = buildDefaultWorkflow(classification);
|
|
180
|
+
workflow = buildDefaultWorkflow(classification, projectConfig.workflow);
|
|
150
181
|
} else if (mode === "full") {
|
|
151
182
|
workflow = buildFullWorkflow();
|
|
152
183
|
}
|
|
153
184
|
|
|
154
|
-
// Find first active
|
|
185
|
+
// Find first active step
|
|
155
186
|
let firstPhase: PhaseName = "plan";
|
|
156
|
-
let
|
|
187
|
+
let firstStep = "clarify";
|
|
157
188
|
|
|
158
189
|
if (workflow) {
|
|
159
190
|
const first = workflow.find((s) => s.included);
|
|
160
191
|
if (first) {
|
|
161
192
|
firstPhase = first.phase;
|
|
162
|
-
|
|
193
|
+
firstStep = first.step;
|
|
163
194
|
}
|
|
164
195
|
}
|
|
165
196
|
|
|
166
197
|
// Build state
|
|
167
198
|
const state: WorkKitState = {
|
|
168
|
-
version:
|
|
199
|
+
version: 2,
|
|
169
200
|
slug,
|
|
170
201
|
branch,
|
|
171
202
|
started: new Date().toISOString(),
|
|
172
203
|
mode: modeLabel,
|
|
173
204
|
...(gated && { gated: true }),
|
|
174
205
|
...(classification && { classification }),
|
|
206
|
+
...(modelPolicy !== "auto" && { modelPolicy }),
|
|
175
207
|
status: "in-progress",
|
|
176
208
|
currentPhase: firstPhase,
|
|
177
|
-
|
|
209
|
+
currentStep: firstStep,
|
|
178
210
|
phases: buildPhases(workflow),
|
|
179
211
|
...(mode === "auto" && workflow && { workflow }),
|
|
180
212
|
loopbacks: [],
|
|
181
213
|
metadata: {
|
|
182
214
|
worktreeRoot,
|
|
183
|
-
mainRepoRoot
|
|
215
|
+
mainRepoRoot,
|
|
184
216
|
},
|
|
185
217
|
};
|
|
186
218
|
|
|
187
219
|
// Ensure .work-kit/ is gitignored (temp working state, not for commits)
|
|
188
|
-
ensureGitignored(worktreeRoot);
|
|
220
|
+
ensureGitignored(worktreeRoot, `${STATE_DIR}/`);
|
|
189
221
|
|
|
190
222
|
// Write state files
|
|
191
223
|
writeState(worktreeRoot, state);
|
|
192
224
|
writeStateMd(worktreeRoot, generateStateMd(slug, branch, modeLabel, description, classification, workflow));
|
|
193
225
|
|
|
226
|
+
const model = resolveModel(state, firstPhase, firstStep);
|
|
227
|
+
|
|
194
228
|
return {
|
|
195
229
|
action: "spawn_agent",
|
|
196
230
|
phase: firstPhase,
|
|
197
|
-
|
|
198
|
-
skillFile: skillFilePath(firstPhase,
|
|
199
|
-
agentPrompt: `You are starting the ${firstPhase} phase. Begin with the ${
|
|
200
|
-
onComplete:
|
|
231
|
+
step: firstStep,
|
|
232
|
+
skillFile: skillFilePath(firstPhase, firstStep),
|
|
233
|
+
agentPrompt: `You are starting the ${firstPhase} phase. Begin with the ${firstStep} step. Read the skill file and follow its instructions. Write outputs to .work-kit/state.md.`,
|
|
234
|
+
onComplete: `${CLI_BINARY} complete ${firstPhase}/${firstStep}`,
|
|
235
|
+
...(model && { model }),
|
|
201
236
|
};
|
|
202
237
|
}
|