kritya 0.8.2-beta
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/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/agent/budget.js +25 -0
- package/dist/agent/compactor.js +78 -0
- package/dist/agent/contextWarning.js +6 -0
- package/dist/agent/killSwitch.js +111 -0
- package/dist/agent/loop.js +543 -0
- package/dist/agent/memory.js +145 -0
- package/dist/agent/plugins.js +408 -0
- package/dist/agent/skills.js +210 -0
- package/dist/agent/skillsCli.js +51 -0
- package/dist/agent/systemPrompt.js +122 -0
- package/dist/agent/tokens.js +55 -0
- package/dist/agent/toolExecutor.js +331 -0
- package/dist/agent/workflow.js +496 -0
- package/dist/agent/worktree.js +116 -0
- package/dist/atomicWrite.js +109 -0
- package/dist/audit/audit.js +230 -0
- package/dist/audit/cli.js +135 -0
- package/dist/commands/custom.js +69 -0
- package/dist/commands/mcpCommand.js +433 -0
- package/dist/commands/pluginsCommand.js +204 -0
- package/dist/commands/registry.js +739 -0
- package/dist/config/config.js +214 -0
- package/dist/config/debug.js +19 -0
- package/dist/config/models.js +82 -0
- package/dist/config/retention.js +24 -0
- package/dist/config/winAcl.js +38 -0
- package/dist/crash.js +102 -0
- package/dist/engine.js +113 -0
- package/dist/git/git.js +46 -0
- package/dist/headless.js +274 -0
- package/dist/hooks/hooks.js +122 -0
- package/dist/index.js +670 -0
- package/dist/lsp/client.js +413 -0
- package/dist/lsp/manager.js +0 -0
- package/dist/lsp/registry.js +62 -0
- package/dist/mcp/callback.js +141 -0
- package/dist/mcp/client.js +945 -0
- package/dist/mcp/login.js +117 -0
- package/dist/mcp/oauth.js +345 -0
- package/dist/mcp/servers.js +114 -0
- package/dist/mcp/spawnWin.js +86 -0
- package/dist/mcp/tokens.js +97 -0
- package/dist/mcp/transport.js +295 -0
- package/dist/net/urlSafety.js +158 -0
- package/dist/permissions/danger.js +96 -0
- package/dist/permissions/permissions.js +62 -0
- package/dist/permissions/rules.js +69 -0
- package/dist/plugins/discover.js +108 -0
- package/dist/plugins/mcp.js +80 -0
- package/dist/provider/client.js +460 -0
- package/dist/provider/switchyardClient.js +46 -0
- package/dist/provider/switchyardSidecar.js +245 -0
- package/dist/provider/textToolCalls.js +140 -0
- package/dist/repomap/repoMap.js +168 -0
- package/dist/repomap/symbols.js +190 -0
- package/dist/session/store.js +328 -0
- package/dist/shell/background.js +131 -0
- package/dist/shell/sandbox.js +396 -0
- package/dist/telemetry/metrics.js +172 -0
- package/dist/telemetry/otlp.js +128 -0
- package/dist/telemetry/tracer.js +240 -0
- package/dist/tools/askUser.js +64 -0
- package/dist/tools/bg.js +59 -0
- package/dist/tools/common.js +158 -0
- package/dist/tools/deepResearch.js +126 -0
- package/dist/tools/diff.js +70 -0
- package/dist/tools/document/docx.js +44 -0
- package/dist/tools/document/pdf.js +190 -0
- package/dist/tools/document/pptx.js +148 -0
- package/dist/tools/document/types.js +26 -0
- package/dist/tools/document/xlsx.js +87 -0
- package/dist/tools/document.js +389 -0
- package/dist/tools/edit.js +56 -0
- package/dist/tools/fetchUrl.js +218 -0
- package/dist/tools/fuzzyMatch.js +74 -0
- package/dist/tools/glob.js +34 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/ignore.js +28 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/ls.js +27 -0
- package/dist/tools/lsp.js +239 -0
- package/dist/tools/notebook.js +215 -0
- package/dist/tools/read.js +32 -0
- package/dist/tools/repoMap.js +26 -0
- package/dist/tools/secretScan.js +143 -0
- package/dist/tools/shell.js +134 -0
- package/dist/tools/skills.js +69 -0
- package/dist/tools/subagent.js +57 -0
- package/dist/tools/tasks.js +43 -0
- package/dist/tools/webSearch.js +97 -0
- package/dist/tools/write.js +47 -0
- package/dist/tools/writeAgent.js +72 -0
- package/dist/trust/aiDisclosure.js +45 -0
- package/dist/trust/mcpTrust.js +111 -0
- package/dist/trust/trust.js +231 -0
- package/dist/types.js +1 -0
- package/dist/ui/AiDisclosurePrompt.js +23 -0
- package/dist/ui/App.js +449 -0
- package/dist/ui/Banner.js +71 -0
- package/dist/ui/ElicitationPrompt.js +46 -0
- package/dist/ui/Markdown.js +148 -0
- package/dist/ui/McpTrustPrompt.js +42 -0
- package/dist/ui/ModelPicker.js +19 -0
- package/dist/ui/PermissionPrompt.js +27 -0
- package/dist/ui/SelectList.js +24 -0
- package/dist/ui/Spinner.js +12 -0
- package/dist/ui/StatusLine.js +11 -0
- package/dist/ui/TranscriptItem.js +13 -0
- package/dist/ui/TrustPrompt.js +12 -0
- package/dist/ui/highlight.js +137 -0
- package/dist/ui/inline.js +241 -0
- package/dist/ui/mermaid.js +98 -0
- package/dist/ui/table.js +148 -0
- package/dist/ui/toolOutputPreview.js +34 -0
- package/dist/ui/useAgent.js +534 -0
- package/dist/ui/useKillSwitch.js +65 -0
- package/dist/ui/useSessionResume.js +39 -0
- package/dist/ui/useUsageBudget.js +149 -0
- package/dist/ui/viewport.js +71 -0
- package/dist/undo/undo.js +293 -0
- package/dist/version.js +4 -0
- package/package.json +122 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const PHASE_ORDER = [
|
|
4
|
+
"brainstorm",
|
|
5
|
+
"spec",
|
|
6
|
+
"plan",
|
|
7
|
+
"build",
|
|
8
|
+
"review",
|
|
9
|
+
"fix",
|
|
10
|
+
];
|
|
11
|
+
/** The slash command that runs each phase, for user-facing guidance. */
|
|
12
|
+
export const PHASE_COMMAND = {
|
|
13
|
+
brainstorm: "/flow-brainstorm",
|
|
14
|
+
spec: "/flow-spec",
|
|
15
|
+
plan: "/flow-plan",
|
|
16
|
+
build: "/flow-build",
|
|
17
|
+
review: "/flow-review",
|
|
18
|
+
fix: "/flow-fix",
|
|
19
|
+
};
|
|
20
|
+
/** One-line summary of what each phase produces, for help text and the system prompt. */
|
|
21
|
+
export const PHASE_SUMMARY = {
|
|
22
|
+
brainstorm: "problem, users, MVP features, recommended stack",
|
|
23
|
+
spec: "goals, non-goals, contracts, data schema, prioritized acceptance criteria, non-functional requirements",
|
|
24
|
+
plan: "architecture and ordered milestones, flagged by risk (runs in read-only plan mode)",
|
|
25
|
+
build: "the application code, with tests per acceptance criterion, written test-first",
|
|
26
|
+
review: "spec-compliance, security, and reliability findings, with a scorecard up top",
|
|
27
|
+
fix: "fixes for the review's findings, each re-verified",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Word budget for each phase's artifact. These docs are read by every phase
|
|
31
|
+
* that follows, so a bloated one is paid for repeatedly — the cap is a cost
|
|
32
|
+
* control, not a style preference.
|
|
33
|
+
*/
|
|
34
|
+
const PHASE_WORD_CAP = {
|
|
35
|
+
brainstorm: 600,
|
|
36
|
+
spec: 1200,
|
|
37
|
+
plan: 1000,
|
|
38
|
+
review: 800,
|
|
39
|
+
fix: 600,
|
|
40
|
+
};
|
|
41
|
+
const STATE_REL = path.join(".kritya", "project.json");
|
|
42
|
+
export function stateFile(workspace) {
|
|
43
|
+
return path.join(workspace, STATE_REL);
|
|
44
|
+
}
|
|
45
|
+
function isPhase(v) {
|
|
46
|
+
return typeof v === "string" && PHASE_ORDER.includes(v);
|
|
47
|
+
}
|
|
48
|
+
/** Parse a phase name from user input (e.g. `/project goto spec`). */
|
|
49
|
+
export function parsePhase(v) {
|
|
50
|
+
const t = v.trim().toLowerCase();
|
|
51
|
+
return isPhase(t) ? t : null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Turn free text into a filesystem-safe project slug (docs/<slug>/…).
|
|
55
|
+
*
|
|
56
|
+
* The 40-character cap is trimmed back to the last whole word rather than cut
|
|
57
|
+
* mid-token — an idea passed straight through used to produce folder names
|
|
58
|
+
* like `a-script-that-reverses-a-string-python-s`.
|
|
59
|
+
*/
|
|
60
|
+
export function slugify(name) {
|
|
61
|
+
const full = name
|
|
62
|
+
.trim()
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
65
|
+
.replace(/^-+|-+$/g, "");
|
|
66
|
+
if (full.length <= SLUG_MAX)
|
|
67
|
+
return full || "project";
|
|
68
|
+
const cut = full.slice(0, SLUG_MAX);
|
|
69
|
+
const lastDash = cut.lastIndexOf("-");
|
|
70
|
+
// Keep whole words, unless the first word alone already exceeds the cap.
|
|
71
|
+
return (lastDash > 0 ? cut.slice(0, lastDash) : cut).replace(/-+$/g, "") || "project";
|
|
72
|
+
}
|
|
73
|
+
const SLUG_MAX = 40;
|
|
74
|
+
/** How many words a `name: idea` prefix may have before it reads as prose. */
|
|
75
|
+
const NAME_PREFIX_MAX_WORDS = 5;
|
|
76
|
+
/**
|
|
77
|
+
* Split `/flow-brainstorm` input into a project name and the idea itself.
|
|
78
|
+
*
|
|
79
|
+
* `reverser: a script that reverses a string` names the project `reverser`.
|
|
80
|
+
* Without a short leading `name:` the whole input is the idea, and the name is
|
|
81
|
+
* derived from it as before — so naming is opt-in and nothing existing breaks.
|
|
82
|
+
*/
|
|
83
|
+
export function parseIdea(input) {
|
|
84
|
+
const text = input.trim();
|
|
85
|
+
const colon = text.indexOf(":");
|
|
86
|
+
if (colon > 0) {
|
|
87
|
+
const prefix = text.slice(0, colon).trim();
|
|
88
|
+
const rest = text.slice(colon + 1).trim();
|
|
89
|
+
const words = prefix.split(/\s+/).filter(Boolean);
|
|
90
|
+
// A prefix that carries sentence punctuation is prose, not a name.
|
|
91
|
+
const looksLikeName = rest.length > 0 &&
|
|
92
|
+
words.length > 0 &&
|
|
93
|
+
words.length <= NAME_PREFIX_MAX_WORDS &&
|
|
94
|
+
prefix.length <= SLUG_MAX &&
|
|
95
|
+
!/[.,;!?—–]/.test(prefix);
|
|
96
|
+
if (looksLikeName)
|
|
97
|
+
return { name: prefix, idea: rest };
|
|
98
|
+
}
|
|
99
|
+
return { name: text, idea: text };
|
|
100
|
+
}
|
|
101
|
+
export function loadProjectState(workspace) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(fs.readFileSync(stateFile(workspace), "utf8"));
|
|
104
|
+
if (!parsed || typeof parsed.name !== "string" || !isPhase(parsed.phase))
|
|
105
|
+
return null;
|
|
106
|
+
return { name: parsed.name, phase: parsed.phase, updatedAt: parsed.updatedAt ?? "" };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function saveProjectState(workspace, name, phase) {
|
|
113
|
+
const state = {
|
|
114
|
+
name: slugify(name),
|
|
115
|
+
phase,
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
fs.mkdirSync(path.dirname(stateFile(workspace)), { recursive: true });
|
|
119
|
+
fs.writeFileSync(stateFile(workspace), JSON.stringify(state, null, 2) + "\n");
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* End the active workflow. The docs/<name>/ artifacts are left alone — only the
|
|
124
|
+
* pointer goes, so the agent stops being told to resume a project the user has
|
|
125
|
+
* moved on from.
|
|
126
|
+
*/
|
|
127
|
+
export function clearProjectState(workspace) {
|
|
128
|
+
try {
|
|
129
|
+
fs.rmSync(stateFile(workspace));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Rename the active project, moving its docs/<name>/ folder with it. Returns an
|
|
138
|
+
* error message rather than throwing, since every caller is a slash command
|
|
139
|
+
* reporting back to the user.
|
|
140
|
+
*
|
|
141
|
+
* Refuses to overwrite an existing folder: a rename that silently merged two
|
|
142
|
+
* projects' artifacts would be indistinguishable from data loss.
|
|
143
|
+
*/
|
|
144
|
+
export function renameProject(workspace, from, to) {
|
|
145
|
+
const next = slugify(to);
|
|
146
|
+
const current = slugify(from);
|
|
147
|
+
if (!to.trim())
|
|
148
|
+
return { ok: false, error: "Usage: /project rename <name>" };
|
|
149
|
+
if (next === current)
|
|
150
|
+
return { ok: true, name: next };
|
|
151
|
+
const state = loadProjectState(workspace);
|
|
152
|
+
if (!state)
|
|
153
|
+
return { ok: false, error: "No active project workflow." };
|
|
154
|
+
const srcDir = path.join(workspace, "docs", current);
|
|
155
|
+
const dstDir = path.join(workspace, "docs", next);
|
|
156
|
+
if (fs.existsSync(dstDir)) {
|
|
157
|
+
return { ok: false, error: `docs/${next}/ already exists — pick another name.` };
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// A project renamed before it wrote anything has no folder to move.
|
|
161
|
+
if (fs.existsSync(srcDir)) {
|
|
162
|
+
fs.mkdirSync(path.dirname(dstDir), { recursive: true });
|
|
163
|
+
fs.renameSync(srcDir, dstDir);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return { ok: false, error: `Could not move docs/${current}/: ${err.message}` };
|
|
168
|
+
}
|
|
169
|
+
saveProjectState(workspace, next, state.phase);
|
|
170
|
+
return { ok: true, name: next };
|
|
171
|
+
}
|
|
172
|
+
/** The phase before `phase` in PHASE_ORDER, or null for the first one. */
|
|
173
|
+
export function previousPhase(phase) {
|
|
174
|
+
const i = PHASE_ORDER.indexOf(phase);
|
|
175
|
+
return i > 0 ? PHASE_ORDER[i - 1] : null;
|
|
176
|
+
}
|
|
177
|
+
/** The phase after `phase` in PHASE_ORDER, or null for the last one. */
|
|
178
|
+
export function nextPhase(phase) {
|
|
179
|
+
const i = PHASE_ORDER.indexOf(phase);
|
|
180
|
+
return i >= 0 && i < PHASE_ORDER.length - 1 ? PHASE_ORDER[i + 1] : null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Relative path of the artifact a phase produces. Build is the exception: it
|
|
184
|
+
* produces application code, not a single document.
|
|
185
|
+
*/
|
|
186
|
+
export function artifactPath(name, phase) {
|
|
187
|
+
if (phase === "build")
|
|
188
|
+
return null;
|
|
189
|
+
return `docs/${slugify(name)}/${phase}.md`;
|
|
190
|
+
}
|
|
191
|
+
/** Whether a phase's artifact has actually been written (non-empty). */
|
|
192
|
+
export function artifactExists(workspace, name, phase) {
|
|
193
|
+
const rel = artifactPath(name, phase);
|
|
194
|
+
if (rel === null)
|
|
195
|
+
return true; // build has no doc to check
|
|
196
|
+
try {
|
|
197
|
+
return fs.statSync(path.join(workspace, rel)).size > 0;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Why `phase` isn't ready to run yet, or null if it is. A phase is ready when
|
|
205
|
+
* the one before it left its artifact behind — otherwise the prompt would tell
|
|
206
|
+
* the agent to "read docs/x/spec.md first" for a file that was never written,
|
|
207
|
+
* and it would improvise the requirements instead.
|
|
208
|
+
*/
|
|
209
|
+
export function phaseBlocker(workspace, name, phase) {
|
|
210
|
+
const prev = previousPhase(phase);
|
|
211
|
+
if (!prev || artifactExists(workspace, name, prev))
|
|
212
|
+
return null;
|
|
213
|
+
const rel = artifactPath(name, prev);
|
|
214
|
+
return (`The ${phase} phase reads ${rel}, which doesn't exist yet. ` +
|
|
215
|
+
`Run ${PHASE_COMMAND[prev]} first (${PHASE_SUMMARY[prev]}).`);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Artifacts that are now older than one they depend on, e.g. spec.md edited
|
|
219
|
+
* after plan.md was already written from it. This never blocks a phase — a
|
|
220
|
+
* stale downstream doc might still be exactly what the user wants — it only
|
|
221
|
+
* warns, at the two points that matter: right before a phase runs (its input
|
|
222
|
+
* may be stale) and in `/project` status (a standing view of the whole chain).
|
|
223
|
+
* `build` has no artifact file to timestamp, so a stale `plan.md` also flags
|
|
224
|
+
* "the code already built from it" once the project has reached `build` or
|
|
225
|
+
* beyond — there is no way to prove the code is out of sync without a file to
|
|
226
|
+
* check, but staying silent would be worse: silent drift between spec and
|
|
227
|
+
* code is exactly the failure mode the whole staged workflow exists to avoid.
|
|
228
|
+
*/
|
|
229
|
+
export function staleArtifacts(workspace, name, currentPhase) {
|
|
230
|
+
const mtime = (phase) => {
|
|
231
|
+
const rel = artifactPath(name, phase);
|
|
232
|
+
if (!rel)
|
|
233
|
+
return null;
|
|
234
|
+
try {
|
|
235
|
+
return fs.statSync(path.join(workspace, rel)).mtimeMs;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const reachedBuild = PHASE_ORDER.indexOf(currentPhase) >= PHASE_ORDER.indexOf("build");
|
|
242
|
+
const warnings = [];
|
|
243
|
+
let planIsStale = false;
|
|
244
|
+
for (let i = 0; i < PHASE_ORDER.length; i++) {
|
|
245
|
+
const upstream = PHASE_ORDER[i];
|
|
246
|
+
const upstreamMtime = mtime(upstream);
|
|
247
|
+
if (upstreamMtime === null)
|
|
248
|
+
continue;
|
|
249
|
+
for (const downstream of PHASE_ORDER.slice(i + 1)) {
|
|
250
|
+
if (downstream === "build")
|
|
251
|
+
continue; // no doc to compare — handled below
|
|
252
|
+
const downstreamRel = artifactPath(name, downstream);
|
|
253
|
+
const downstreamMtime = mtime(downstream);
|
|
254
|
+
if (downstreamMtime === null)
|
|
255
|
+
continue;
|
|
256
|
+
if (downstreamMtime < upstreamMtime) {
|
|
257
|
+
const upstreamRel = artifactPath(name, upstream);
|
|
258
|
+
warnings.push(`⚠ ${downstreamRel} was written before ${upstreamRel}'s latest change — it may be stale.`);
|
|
259
|
+
if (downstream === "plan")
|
|
260
|
+
planIsStale = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Only plan.md feeds build directly; spec/brainstorm feed build only through
|
|
265
|
+
// plan.md, so this fires once, off of plan.md's own staleness above, rather
|
|
266
|
+
// than once per upstream phase.
|
|
267
|
+
if (planIsStale && reachedBuild) {
|
|
268
|
+
warnings.push(`⚠ plan.md changed after the code was built from it — the build may no longer match.`);
|
|
269
|
+
}
|
|
270
|
+
return warnings;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* During plan mode all mutating tools are blocked — except the active project's
|
|
274
|
+
* own planning documents. Scoping this to docs/<slug>/ matters: plan mode's
|
|
275
|
+
* promise is that nothing outside the plan changes, and a workspace-wide
|
|
276
|
+
* docs/**\/*.md exemption would let a read-only phase overwrite unrelated
|
|
277
|
+
* documentation (a project's ARCHITECTURE.md, its README docs) as a side
|
|
278
|
+
* effect. With no active project there is no planning doc to write, so nothing
|
|
279
|
+
* is exempt.
|
|
280
|
+
*/
|
|
281
|
+
export function isPlanningDocWrite(workspace, toolName, args, projectName) {
|
|
282
|
+
if (toolName !== "write_file" && toolName !== "edit_file")
|
|
283
|
+
return false;
|
|
284
|
+
if (!projectName)
|
|
285
|
+
return false;
|
|
286
|
+
const raw = String(args.path ?? "");
|
|
287
|
+
if (!raw)
|
|
288
|
+
return false;
|
|
289
|
+
// Resolve exactly the way the write tools do (resolveSafe -> path.resolve),
|
|
290
|
+
// then compare relative to the workspace. Models routinely pass an absolute
|
|
291
|
+
// path even when told the argument is workspace-relative, and a plain string
|
|
292
|
+
// prefix test misses those — which left plan mode blocking the very document
|
|
293
|
+
// the plan phase exists to write.
|
|
294
|
+
const rel = path.relative(path.resolve(workspace), path.resolve(workspace, raw));
|
|
295
|
+
const p = rel.split(path.sep).join("/");
|
|
296
|
+
// Anything that resolved outside the workspace is not a planning doc.
|
|
297
|
+
if (!p || p === ".." || p.startsWith("../"))
|
|
298
|
+
return false;
|
|
299
|
+
const dir = `docs/${slugify(projectName)}/`;
|
|
300
|
+
if (!p.startsWith(dir))
|
|
301
|
+
return false;
|
|
302
|
+
const rest = p.slice(dir.length);
|
|
303
|
+
return rest.endsWith(".md") && rest.length > 3 && !rest.startsWith(".");
|
|
304
|
+
}
|
|
305
|
+
function capLine(phase) {
|
|
306
|
+
const cap = PHASE_WORD_CAP[phase];
|
|
307
|
+
return cap
|
|
308
|
+
? `Keep it under ~${cap} words — every later phase reads this document, so length here is paid for several times over. Be dense, not verbose.\n`
|
|
309
|
+
: "";
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Write first, ask second. Models otherwise present the artifact's contents in
|
|
313
|
+
* chat and ask for approval *before* writing it — which strands the phase: the
|
|
314
|
+
* user approves, but the file the next phase reads was never created, and in
|
|
315
|
+
* the plan phase the belated write lands after plan mode has already been
|
|
316
|
+
* turned off by the next command.
|
|
317
|
+
*/
|
|
318
|
+
function approvalLine(phase, artifact) {
|
|
319
|
+
const next = nextPhase(phase);
|
|
320
|
+
const write = artifact
|
|
321
|
+
? `Write ${artifact} BEFORE you ask for anything. Approval comes after the file exists — ` +
|
|
322
|
+
`never show the document in chat and ask whether to save it. `
|
|
323
|
+
: "";
|
|
324
|
+
if (!next)
|
|
325
|
+
return `${write}Finish by summarizing the findings for the user.`;
|
|
326
|
+
return (`${write}Then summarize what you produced in a few lines and ask the user to approve ` +
|
|
327
|
+
`before the ${next} phase (they will run ${PHASE_COMMAND[next]}). Do not advance on your own.`);
|
|
328
|
+
}
|
|
329
|
+
/** The instruction sent to the agent to carry out a given phase. */
|
|
330
|
+
export function phasePrompt(name, phase, userInput) {
|
|
331
|
+
const slug = slugify(name);
|
|
332
|
+
const extra = userInput.trim() ? `\n\nUser input for this phase:\n${userInput.trim()}` : "";
|
|
333
|
+
const cap = capLine(phase);
|
|
334
|
+
const approve = approvalLine(phase, artifactPath(name, phase));
|
|
335
|
+
switch (phase) {
|
|
336
|
+
case "brainstorm":
|
|
337
|
+
return (`PROJECT WORKFLOW — BRAINSTORM phase for project "${slug}".\n` +
|
|
338
|
+
`Be a sharp product thinking-partner. Clarify the problem, the target users, the core ` +
|
|
339
|
+
`features (MVP vs. later), and recommend a concrete tech stack with a short rationale. ` +
|
|
340
|
+
`If you can make a sensible default choice (e.g. a common tech-stack pick), make it and ` +
|
|
341
|
+
`state your reasoning in the doc rather than asking — reserve questions for the make-or` +
|
|
342
|
+
`-break ones you genuinely cannot guess (who the user is, what the MVP must include). For ` +
|
|
343
|
+
`each one, use ask_user with a short list of concrete options (e.g. candidate stacks or ` +
|
|
344
|
+
`feature scopes) rather than an open-ended question in chat — the user answers faster from ` +
|
|
345
|
+
`a list, and can always type their own answer via the option ask_user adds automatically. ` +
|
|
346
|
+
`When recommending a stack, name the trade-off in one clause (e.g. "SQLite: simplest, no ` +
|
|
347
|
+
`multi-user support" vs. "Postgres: more setup, scales past one writer") rather than just ` +
|
|
348
|
+
`announcing a pick. Once resolved, write your synthesis to docs/${slug}/brainstorm.md.\n` +
|
|
349
|
+
cap +
|
|
350
|
+
`Do NOT write application code, do NOT design the architecture, and do NOT specify ` +
|
|
351
|
+
`interfaces yet — this phase settles direction only.\n` +
|
|
352
|
+
approve +
|
|
353
|
+
extra);
|
|
354
|
+
case "spec":
|
|
355
|
+
return (`PROJECT WORKFLOW — SPEC phase for project "${slug}".\n` +
|
|
356
|
+
`Read docs/${slug}/brainstorm.md first — that is your only required input. Write a ` +
|
|
357
|
+
`concrete spec/PRD to docs/${slug}/spec.md covering: goals, non-goals, user stories, ` +
|
|
358
|
+
`API/interface contracts, the data schema, success metrics, and acceptance criteria.\n` +
|
|
359
|
+
`Acceptance criteria are the contract the build and review phases are held to, so make ` +
|
|
360
|
+
`each one specific and checkable — a criterion that cannot fail a test is not a criterion. ` +
|
|
361
|
+
`Label them AC1, AC2, AC3, … under an "Acceptance criteria" heading; later phases cite ` +
|
|
362
|
+
`those labels, so they must be stable identifiers, not positions in the document. Mark ` +
|
|
363
|
+
`each one MUST or LATER (must-have for this build, vs. a later iteration) — if build ever ` +
|
|
364
|
+
`runs short on time or scope, LATER is what gets cut first, and that decision belongs here, ` +
|
|
365
|
+
`not left for build to guess.\n` +
|
|
366
|
+
`Before finalizing, use ask_user to ask whether this project: handles sensitive or ` +
|
|
367
|
+
`personal data, needs auth/access control, takes input from outside the user's own ` +
|
|
368
|
+
`machine, has real reliability or performance stakes, or needs CI/logging from day one. ` +
|
|
369
|
+
`If any answer is yes, add a "Non-functional requirements" section with concrete, ` +
|
|
370
|
+
`checkable requirements for it — label security ones SEC1, SEC2, … and reliability/` +
|
|
371
|
+
`performance/observability ones REL1, REL2, …, same stable-identifier rule as the ACs. ` +
|
|
372
|
+
`If none apply, write one line saying so and move on — do not invent security or ` +
|
|
373
|
+
`reliability requirements a small project doesn't need.\n` +
|
|
374
|
+
cap +
|
|
375
|
+
`Do NOT design the architecture, choose a folder layout, or sequence the work — that is ` +
|
|
376
|
+
`the plan phase's job. Do NOT write application code.\n` +
|
|
377
|
+
approve +
|
|
378
|
+
extra);
|
|
379
|
+
case "plan":
|
|
380
|
+
return (`PROJECT WORKFLOW — PLAN phase for project "${slug}". Plan mode is ON (read-only for code).\n` +
|
|
381
|
+
`Read docs/${slug}/spec.md first — that is your only required input. Design how to build ` +
|
|
382
|
+
`it: folder/module layout, dependencies, external services, and an ordered, ` +
|
|
383
|
+
`milestone-based build sequence. Each milestone must cite the acceptance criteria it ` +
|
|
384
|
+
`satisfies by their AC labels (e.g. "satisfies AC2, AC5") — never by line numbers or ` +
|
|
385
|
+
`positions in spec.md, which change the moment the file is edited — and be ` +
|
|
386
|
+
`independently testable.\n` +
|
|
387
|
+
`Note which milestones are genuinely independent of each other — the build phase uses ` +
|
|
388
|
+
`that to parallelize.\n` +
|
|
389
|
+
`Tag each milestone RISKY or ROUTINE. RISKY means it depends on something you haven't ` +
|
|
390
|
+
`confirmed works — an external API, a library behavior, a performance assumption — or ` +
|
|
391
|
+
`touches a part of the system where a wrong guess is expensive to unwind. Say what makes ` +
|
|
392
|
+
`it risky in one clause. This is what tells the user, and the build phase, where to slow ` +
|
|
393
|
+
`down and check assumptions instead of plowing straight through.\n` +
|
|
394
|
+
`If spec.md has a "Non-functional requirements" section: for each milestone that cites a ` +
|
|
395
|
+
`SEC or REL label, note the trust boundary or failure mode it implies (e.g. "this endpoint ` +
|
|
396
|
+
`takes unauthenticated input" or "this call can time out") and flag any dependency it ` +
|
|
397
|
+
`needs that's worth vetting for license or maintenance risk before adopting it. If spec.md ` +
|
|
398
|
+
`has no such section, skip this — don't invent security or reliability process for a ` +
|
|
399
|
+
`project that doesn't need it.\n` +
|
|
400
|
+
`Write the plan to docs/${slug}/plan.md — writing Markdown under docs/${slug}/ is allowed ` +
|
|
401
|
+
`in plan mode; application code and shell are still blocked. That write will succeed, so ` +
|
|
402
|
+
`do not ask the user to turn plan mode off in order to save the plan, and use the ` +
|
|
403
|
+
`workspace-relative path exactly as given.\n` +
|
|
404
|
+
cap +
|
|
405
|
+
`Do NOT restate the spec's requirements, contracts, or data schema — reference them by ` +
|
|
406
|
+
`number instead. Duplicated requirements drift.\n` +
|
|
407
|
+
approve +
|
|
408
|
+
extra);
|
|
409
|
+
case "build":
|
|
410
|
+
return (`PROJECT WORKFLOW — BUILD phase for project "${slug}". Plan mode is OFF.\n` +
|
|
411
|
+
`Read docs/${slug}/plan.md — it is your build order. Call update_tasks first with the full ` +
|
|
412
|
+
`milestone breakdown (one task per milestone), then keep it current as you go: mark a ` +
|
|
413
|
+
`milestone in_progress the moment you start it and done the moment its tests pass — call ` +
|
|
414
|
+
`update_tasks again each time a milestone's status changes, not just once at the start. ` +
|
|
415
|
+
`This is the user's only live view into a build that can otherwise run for a long time in ` +
|
|
416
|
+
`silence, so do not let the checklist go stale.\n` +
|
|
417
|
+
`RISKY milestones (per plan.md's tags): confirm the assumption that makes them risky before ` +
|
|
418
|
+
`writing the rest of the milestone, so a wrong guess is caught early and cheaply, not after ` +
|
|
419
|
+
`everything downstream is built on top of it.\n` +
|
|
420
|
+
`Tests are part of the deliverable, not an afterthought: for each milestone, consult the ` +
|
|
421
|
+
`AC-labelled acceptance criteria it cites in docs/${slug}/spec.md (read just those, not the ` +
|
|
422
|
+
`whole file again). Write the test before the code it tests: write it, run it, confirm it ` +
|
|
423
|
+
`fails for the right reason, then write the minimal implementation that makes it pass. Do ` +
|
|
424
|
+
`not write the implementation first and backfill a test that already passes — that proves ` +
|
|
425
|
+
`nothing. Do not mark a milestone done until its tests actually run and pass. Report ` +
|
|
426
|
+
`pass/fail per milestone, not just what you changed.\n` +
|
|
427
|
+
`If a milestone cites a SEC or REL label from spec.md's Non-functional requirements ` +
|
|
428
|
+
`section, also write a negative/failure-path test for it — invalid input, an unauthorized ` +
|
|
429
|
+
`attempt, a timeout or dependency failure, whatever the label implies — not just the ` +
|
|
430
|
+
`happy-path test for its AC. A milestone touching sensitive data or access control is not ` +
|
|
431
|
+
`done until that test exists and passes too.\n` +
|
|
432
|
+
`If a milestone's tests fail twice in a row after genuine fix attempts, stop working that ` +
|
|
433
|
+
`milestone: mark it blocked in the task list with why, move on to milestones that don't ` +
|
|
434
|
+
`depend on it, and report the block to the user at the end instead of retrying indefinitely.\n` +
|
|
435
|
+
`If the workspace is a git repository and the plan marks milestones as independent, ` +
|
|
436
|
+
`dispatch them with spawn_write_agent in parallel — each subagent gets a fresh context ` +
|
|
437
|
+
`and its own branch, which keeps this conversation from accumulating the full text of ` +
|
|
438
|
+
`every file you touch. Give each one the milestone's criteria verbatim; never a summary ` +
|
|
439
|
+
`of them. Build sequentially otherwise.\n` +
|
|
440
|
+
approve +
|
|
441
|
+
extra);
|
|
442
|
+
case "review":
|
|
443
|
+
return (`PROJECT WORKFLOW — REVIEW phase for project "${slug}".\n` +
|
|
444
|
+
`Review what the build phase produced. Dispatch all of these as read-only subagents in ` +
|
|
445
|
+
`one spawn_agent call so they run concurrently and their tool output never enters this ` +
|
|
446
|
+
`conversation:\n` +
|
|
447
|
+
` 1. SPEC COMPLIANCE — check the implementation against every AC-labelled acceptance ` +
|
|
448
|
+
`criterion in docs/${slug}/spec.md. For each: met, not met, or partially met, with the ` +
|
|
449
|
+
`file and line that decides it. Confirm the tests for each criterion exist and pass.\n` +
|
|
450
|
+
` 2. SECURITY — review the actual code and diffs for injection, authentication and ` +
|
|
451
|
+
`authorization gaps, secret handling, unsafe deserialization, path traversal, SSRF, ` +
|
|
452
|
+
`dependency risk, and missing input validation. Report concrete exploitable findings ` +
|
|
453
|
+
`with file and line, not generic advice.\n` +
|
|
454
|
+
` 3. RELIABILITY — review error handling, edge cases, resource cleanup, and whether every ` +
|
|
455
|
+
`REL-labelled requirement in docs/${slug}/spec.md's Non-functional requirements section ` +
|
|
456
|
+
`(if any) is actually met. Report concrete failure scenarios — what input or condition ` +
|
|
457
|
+
`breaks it, and what happens when it does — with file and line, not generic advice. If ` +
|
|
458
|
+
`spec.md has no REL labels, still check for unhandled errors and missing cleanup; skip ` +
|
|
459
|
+
`only the requirement-compliance half of the remit.\n` +
|
|
460
|
+
`Each subagent starts with no context, so give it the project slug, the paths it needs, ` +
|
|
461
|
+
`and its full remit in the task string.\n` +
|
|
462
|
+
`Open docs/${slug}/review.md with a short SCORECARD before the detailed findings — one ` +
|
|
463
|
+
`line, e.g. "4/6 acceptance criteria met · 2 security findings (1 high, 1 medium) · 1 ` +
|
|
464
|
+
`reliability finding" — so the headline is visible without reading the whole document. ` +
|
|
465
|
+
`Then list the findings grouped by ` +
|
|
466
|
+
`severity, each with a file:line and a concrete suggested fix. State plainly whether the ` +
|
|
467
|
+
`build satisfies the spec. Do not fix anything in this phase — reviewing and fixing in one ` +
|
|
468
|
+
`pass produces neither a trustworthy review nor a reviewed fix.\n` +
|
|
469
|
+
cap +
|
|
470
|
+
approve +
|
|
471
|
+
extra);
|
|
472
|
+
case "fix":
|
|
473
|
+
return (`PROJECT WORKFLOW — FIX phase for project "${slug}". Plan mode is OFF.\n` +
|
|
474
|
+
`Read docs/${slug}/review.md — it lists every finding from the review phase. Address each ` +
|
|
475
|
+
`one: not-met or partially-met acceptance criteria, and every security finding, in severity ` +
|
|
476
|
+
`order. Skip only findings the user has explicitly told you to leave (say which and why in ` +
|
|
477
|
+
`the writeup); everything else gets fixed.\n` +
|
|
478
|
+
`For each finding you fix, note what changed and why in a few words — this becomes ` +
|
|
479
|
+
`docs/${slug}/fix.md, so keep the running log as you go rather than reconstructing it at ` +
|
|
480
|
+
`the end.\n` +
|
|
481
|
+
`After the fixes, dispatch a read-only subagent to re-check only what you touched: give it ` +
|
|
482
|
+
`the specific findings you addressed and the files you changed, and have it confirm each ` +
|
|
483
|
+
`finding is actually resolved (tests pass, the AC is now met, the vulnerability is closed) ` +
|
|
484
|
+
`rather than re-running the full spec-compliance and security sweep from scratch. Record its ` +
|
|
485
|
+
`verdict per finding — fixed, still open, or newly broken — in docs/${slug}/fix.md.\n` +
|
|
486
|
+
`If anything is still open after the subagent's check, say so plainly; do not report success ` +
|
|
487
|
+
`on a finding that didn't actually verify as fixed.\n` +
|
|
488
|
+
cap +
|
|
489
|
+
`Write docs/${slug}/fix.md BEFORE you ask for anything. Approval comes after the file ` +
|
|
490
|
+
`exists — never show the writeup in chat and ask whether to save it. Then summarize the ` +
|
|
491
|
+
`outcome in a few lines. If anything is still open, tell the user they can run ` +
|
|
492
|
+
`${PHASE_COMMAND.review} to re-check the whole build, or run ${PHASE_COMMAND.fix} again ` +
|
|
493
|
+
`once they've decided how to handle what's left.\n` +
|
|
494
|
+
extra);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { debugLog } from "../config/debug.js";
|
|
7
|
+
function git(cwd, args, timeout = 15_000) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execFileSync("git", args, {
|
|
10
|
+
cwd,
|
|
11
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12
|
+
timeout,
|
|
13
|
+
}).toString();
|
|
14
|
+
return { ok: true, out };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
18
|
+
? String(err.stderr)
|
|
19
|
+
: "";
|
|
20
|
+
const message = stderr.trim() || (err instanceof Error ? err.message : String(err));
|
|
21
|
+
return { ok: false, out: message };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Whether `workspace` is inside a git working tree (required for isolated write subagents). */
|
|
25
|
+
export function isGitRepo(workspace) {
|
|
26
|
+
return git(workspace, ["rev-parse", "--is-inside-work-tree"]).ok;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates a fresh branch + worktree off the current HEAD so a write subagent
|
|
30
|
+
* can edit and run commands without ever touching the user's real working
|
|
31
|
+
* tree. Returns null if worktree creation failed (e.g. dirty index conflict,
|
|
32
|
+
* git missing, or an existing branch name collision — vanishingly unlikely
|
|
33
|
+
* given the random suffix, but handled defensively).
|
|
34
|
+
*/
|
|
35
|
+
export function createWorktree(workspace) {
|
|
36
|
+
const headRes = git(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
37
|
+
const baseBranch = headRes.ok ? headRes.out.trim() : "HEAD";
|
|
38
|
+
const id = `${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}`;
|
|
39
|
+
const branch = `kritya/agent-${id}`;
|
|
40
|
+
const root = path.join(os.tmpdir(), "kritya-worktrees");
|
|
41
|
+
const dir = path.join(root, id);
|
|
42
|
+
try {
|
|
43
|
+
fs.mkdirSync(root, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
debugLog(`createWorktree mkdir(${root})`, err);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const add = git(workspace, ["worktree", "add", "-b", branch, dir, "HEAD"]);
|
|
50
|
+
if (!add.ok)
|
|
51
|
+
return null;
|
|
52
|
+
return { dir, branch, baseBranch };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stages and commits whatever the subagent changed inside its worktree, so
|
|
56
|
+
* the diff survives after the worktree checkout is removed (it lives on in
|
|
57
|
+
* the branch's history). Returns "clean" if there was nothing to commit,
|
|
58
|
+
* "committed" on success, or "failed" (e.g. a pre-commit hook rejected it) —
|
|
59
|
+
* callers must not discard the worktree on "failed" so the work isn't lost.
|
|
60
|
+
*
|
|
61
|
+
* Signs with `-c commit.gpgsign=false`: this is an internal bookkeeping
|
|
62
|
+
* commit on a scratch branch, not something attributed to the user — it
|
|
63
|
+
* exists only so the subagent's diff survives worktree teardown, and the
|
|
64
|
+
* user re-commits (with their own signing) when they actually merge it. A
|
|
65
|
+
* user with `commit.gpgsign=true` set globally but not overridden in this
|
|
66
|
+
* repo would otherwise hit an interactive GPG passphrase prompt with no
|
|
67
|
+
* terminal to answer it on, silently losing every write subagent's work.
|
|
68
|
+
*/
|
|
69
|
+
export function commitWorktree(wt, message) {
|
|
70
|
+
const status = git(wt.dir, ["status", "--porcelain"]);
|
|
71
|
+
if (status.ok && !status.out.trim())
|
|
72
|
+
return "clean";
|
|
73
|
+
git(wt.dir, ["add", "-A"]);
|
|
74
|
+
const commit = git(wt.dir, ["-c", "commit.gpgsign=false", "commit", "-m", message]);
|
|
75
|
+
return commit.ok ? "committed" : "failed";
|
|
76
|
+
}
|
|
77
|
+
/** Diffstat of the subagent's branch against the branch it was forked from. */
|
|
78
|
+
export function worktreeDiffStat(workspace, wt) {
|
|
79
|
+
const res = git(workspace, ["diff", `${wt.baseBranch}...${wt.branch}`, "--stat"]);
|
|
80
|
+
return res.ok ? res.out.trim() : "";
|
|
81
|
+
}
|
|
82
|
+
/** Synchronous sleep — used only for a short retry backoff on a flaky `git branch -D`. */
|
|
83
|
+
function sleepSync(ms) {
|
|
84
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tears down a worktree checkout. `deleteBranch` should be true only when the
|
|
88
|
+
* branch made no changes worth keeping — otherwise leave the branch in place
|
|
89
|
+
* so the user can review/merge it later (`git diff <base>...<branch>`,
|
|
90
|
+
* `git merge <branch>`).
|
|
91
|
+
*
|
|
92
|
+
* Returns false if `deleteBranch` was requested but the branch still exists
|
|
93
|
+
* afterward (e.g. a transient lock on the ref, more common on network/9p
|
|
94
|
+
* filesystems) — callers should surface this rather than silently leaving an
|
|
95
|
+
* empty orphaned branch with no indication anything went wrong.
|
|
96
|
+
*/
|
|
97
|
+
export function removeWorktree(workspace, wt, deleteBranch) {
|
|
98
|
+
git(workspace, ["worktree", "remove", "--force", wt.dir]);
|
|
99
|
+
try {
|
|
100
|
+
fs.rmSync(wt.dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// best-effort cleanup; a leftover temp dir is harmless
|
|
104
|
+
}
|
|
105
|
+
if (!deleteBranch)
|
|
106
|
+
return true;
|
|
107
|
+
git(workspace, ["branch", "-D", wt.branch]);
|
|
108
|
+
if (!git(workspace, ["rev-parse", "--verify", wt.branch]).ok)
|
|
109
|
+
return true;
|
|
110
|
+
// First attempt raced with worktree teardown finishing (ref still locked) —
|
|
111
|
+
// one short retry clears it in practice; if not, report it rather than
|
|
112
|
+
// leave a silently-orphaned empty branch.
|
|
113
|
+
sleepSync(150);
|
|
114
|
+
git(workspace, ["branch", "-D", wt.branch]);
|
|
115
|
+
return !git(workspace, ["rev-parse", "--verify", wt.branch]).ok;
|
|
116
|
+
}
|