@try-works/dsh-recursive-mode 0.1.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/cordis.patch.yml +12 -0
- package/lib/bootstrap.d.ts +35 -0
- package/lib/client/board.d.ts +10 -0
- package/lib/client/contract.d.ts +51 -0
- package/lib/client/derive.d.ts +92 -0
- package/lib/client/index.d.ts +21 -0
- package/lib/client/inspector.d.ts +10 -0
- package/lib/client/node.d.ts +71 -0
- package/lib/client/settings.d.ts +6 -0
- package/lib/client/slots.d.ts +7 -0
- package/lib/client/strip.d.ts +7 -0
- package/lib/client.d.ts +10 -0
- package/lib/client.js +490 -0
- package/lib/closeout.d.ts +23 -0
- package/lib/commands.d.ts +51 -0
- package/lib/delegation.d.ts +92 -0
- package/lib/enforcement.d.ts +53 -0
- package/lib/events.d.ts +173 -0
- package/lib/handoff.d.ts +51 -0
- package/lib/index.d.ts +40 -0
- package/lib/lifecycle.d.ts +107 -0
- package/lib/lock.d.ts +92 -0
- package/lib/policy.d.ts +12 -0
- package/lib/projection.d.ts +29 -0
- package/lib/recursive_closeout.tool.d.ts +8 -0
- package/lib/recursive_init.tool.d.ts +2 -0
- package/lib/recursive_lint.tool.d.ts +2 -0
- package/lib/recursive_lock.tool.d.ts +2 -0
- package/lib/recursive_scratch.tool.d.ts +7 -0
- package/lib/recursive_status.tool.d.ts +2 -0
- package/lib/review.d.ts +39 -0
- package/lib/router.d.ts +77 -0
- package/lib/run.d.ts +29 -0
- package/lib/runtime.d.ts +241 -0
- package/lib/scratch.d.ts +18 -0
- package/lib/status.d.ts +19 -0
- package/lib/types.d.ts +104 -0
- package/lib/workspace.d.ts +50 -0
- package/package.json +119 -0
- package/preset/recursive/agent.cordis.yml +282 -0
- package/preset/recursive/preset.yml +3 -0
- package/scripts/install-recursive-mode.ps1 +956 -0
- package/scripts/install-recursive-mode.py +750 -0
- package/scripts/lint-recursive-run.py +2868 -0
- package/scripts/recursive-closeout.py +541 -0
- package/scripts/recursive-init.py +356 -0
- package/scripts/recursive-lock.py +302 -0
- package/scripts/recursive-status.py +2124 -0
- package/scripts/recursive_phase_rules.py +367 -0
- package/scripts/recursive_router_lib.py +2282 -0
- package/scripts/test-recursive-mode-smoke.ts +204 -0
- package/scripts/verify-locks.py +353 -0
- package/src/bootstrap.ts +118 -0
- package/src/client/board.tsx +61 -0
- package/src/client/contract.ts +58 -0
- package/src/client/derive.ts +241 -0
- package/src/client/index.ts +28 -0
- package/src/client/inspector.tsx +49 -0
- package/src/client/node.ts +156 -0
- package/src/client/settings.tsx +18 -0
- package/src/client/slots.ts +67 -0
- package/src/client/strip.tsx +28 -0
- package/src/client.ts +11 -0
- package/src/closeout.ts +183 -0
- package/src/commands.ts +142 -0
- package/src/delegation.ts +306 -0
- package/src/enforcement.ts +180 -0
- package/src/events.ts +173 -0
- package/src/handoff.ts +165 -0
- package/src/index.ts +283 -0
- package/src/lifecycle.ts +235 -0
- package/src/lock.ts +369 -0
- package/src/policy.ts +56 -0
- package/src/projection.ts +237 -0
- package/src/recursive_closeout.tool.ts +35 -0
- package/src/recursive_init.tool.ts +28 -0
- package/src/recursive_lint.tool.ts +29 -0
- package/src/recursive_lock.tool.ts +33 -0
- package/src/recursive_scratch.tool.ts +42 -0
- package/src/recursive_status.tool.ts +24 -0
- package/src/review.ts +178 -0
- package/src/router.ts +197 -0
- package/src/run.ts +85 -0
- package/src/runtime.ts +564 -0
- package/src/scratch.ts +85 -0
- package/src/status.ts +194 -0
- package/src/types.ts +112 -0
- package/src/workspace.ts +67 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@try-works/dsh-recursive-mode",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/client/derive.ts
|
|
9
|
+
const KANBAN_LANES = [
|
|
10
|
+
{
|
|
11
|
+
id: "0",
|
|
12
|
+
label: "Worktree & Requirements",
|
|
13
|
+
groups: ["00"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "1/1.5",
|
|
17
|
+
label: "Analysis",
|
|
18
|
+
groups: ["01", "01.5"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "2",
|
|
22
|
+
label: "TO-BE Plan",
|
|
23
|
+
groups: ["02"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "3/3.5",
|
|
27
|
+
label: "Implementation + Code Review",
|
|
28
|
+
groups: ["03", "03.5"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "4",
|
|
32
|
+
label: "Tests",
|
|
33
|
+
groups: ["04"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "5",
|
|
37
|
+
label: "Manual QA",
|
|
38
|
+
groups: ["05"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "6-8",
|
|
42
|
+
label: "Closeout",
|
|
43
|
+
groups: [
|
|
44
|
+
"06",
|
|
45
|
+
"07",
|
|
46
|
+
"08"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
const GROUP_ORDER = [
|
|
51
|
+
"00",
|
|
52
|
+
"01",
|
|
53
|
+
"01.5",
|
|
54
|
+
"02",
|
|
55
|
+
"03",
|
|
56
|
+
"03.5",
|
|
57
|
+
"04",
|
|
58
|
+
"05",
|
|
59
|
+
"06",
|
|
60
|
+
"07",
|
|
61
|
+
"08"
|
|
62
|
+
];
|
|
63
|
+
/** Mandatory phase groups: a run is only 'locked'-valid when every one of these is present and LOCKED (01.5 is optional). */
|
|
64
|
+
const MANDATORY_GROUPS = [
|
|
65
|
+
"00",
|
|
66
|
+
"01",
|
|
67
|
+
"02",
|
|
68
|
+
"03",
|
|
69
|
+
"03.5",
|
|
70
|
+
"04",
|
|
71
|
+
"05",
|
|
72
|
+
"06",
|
|
73
|
+
"07",
|
|
74
|
+
"08"
|
|
75
|
+
];
|
|
76
|
+
/** Map a group id to its ordinal for stable ordering. */
|
|
77
|
+
const GROUP_INDEX$1 = new Map(GROUP_ORDER.map((g, i) => [g, i]));
|
|
78
|
+
/** Group id for a phase filename (e.g. '03.5-code-review.md' -> '03.5'); null for non-phase names. */
|
|
79
|
+
function phaseGroupOf$1(phase) {
|
|
80
|
+
const m = /^(\d{2}(?:\.\d)?)(?:-|$)/.exec(phase);
|
|
81
|
+
if (m === null) return null;
|
|
82
|
+
const g = m[1];
|
|
83
|
+
return GROUP_INDEX$1.has(g) ? g : null;
|
|
84
|
+
}
|
|
85
|
+
/** Kanban lane id for a phase filename. */
|
|
86
|
+
function laneOf(phase) {
|
|
87
|
+
const g = phaseGroupOf$1(phase);
|
|
88
|
+
if (g === null) return null;
|
|
89
|
+
for (const lane of KANBAN_LANES) if (lane.groups.includes(g)) return lane.id;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The run's current-phase lane: the highest-ordered phase group present in the
|
|
94
|
+
* card wins (a run is "at" its furthest phase). No phases -> lane 0.
|
|
95
|
+
*/
|
|
96
|
+
function columnForRun(card) {
|
|
97
|
+
let best = null;
|
|
98
|
+
let bestIndex = -1;
|
|
99
|
+
for (const key of Object.keys(card.phases)) {
|
|
100
|
+
const g = phaseGroupOf$1(key);
|
|
101
|
+
if (g === null) continue;
|
|
102
|
+
const idx = GROUP_INDEX$1.get(g) ?? -1;
|
|
103
|
+
if (idx > bestIndex) {
|
|
104
|
+
bestIndex = idx;
|
|
105
|
+
best = g;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (best === null) return "0";
|
|
109
|
+
return laneOf(best + "-x") ?? "0";
|
|
110
|
+
}
|
|
111
|
+
/** Derive §11.4 presentation facts from one card (no fs, no session). */
|
|
112
|
+
function cardFacts(card) {
|
|
113
|
+
const groups = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const key of Object.keys(card.phases)) {
|
|
115
|
+
const g = phaseGroupOf$1(key);
|
|
116
|
+
if (g !== null) groups.add(g);
|
|
117
|
+
}
|
|
118
|
+
const lockedGroups = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const key of Object.keys(card.phases)) {
|
|
120
|
+
const g = phaseGroupOf$1(key);
|
|
121
|
+
if (g !== null && card.phases[key].status === "LOCKED") lockedGroups.add(g);
|
|
122
|
+
}
|
|
123
|
+
const lockedCount = lockedGroups.size;
|
|
124
|
+
const totalPhases = groups.size;
|
|
125
|
+
const subagents = Object.values(card.subagents).map((s) => ({
|
|
126
|
+
childId: s.childId,
|
|
127
|
+
role: s.role,
|
|
128
|
+
provider: s.provider,
|
|
129
|
+
status: s.status ?? "done"
|
|
130
|
+
}));
|
|
131
|
+
let currentPhase = null;
|
|
132
|
+
let currentIdx = -1;
|
|
133
|
+
for (const key of Object.keys(card.phases)) {
|
|
134
|
+
const g = phaseGroupOf$1(key);
|
|
135
|
+
if (g === null) continue;
|
|
136
|
+
const idx = GROUP_INDEX$1.get(g) ?? -1;
|
|
137
|
+
if (idx > currentIdx) {
|
|
138
|
+
currentIdx = idx;
|
|
139
|
+
currentPhase = key;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const tampered = Object.keys(card.tampers).length > 0;
|
|
143
|
+
const allMandatoryLocked = MANDATORY_GROUPS.every((g) => {
|
|
144
|
+
const row = Object.entries(card.phases).find(([key]) => phaseGroupOf$1(key) === g);
|
|
145
|
+
return row !== void 0 && row[1].status === "LOCKED";
|
|
146
|
+
});
|
|
147
|
+
const lockValidity = tampered ? "tampered" : allMandatoryLocked ? "locked" : "in-progress";
|
|
148
|
+
return {
|
|
149
|
+
runId: card.runId,
|
|
150
|
+
worktreeRoot: card.worktreeRoot,
|
|
151
|
+
repo: card.repo,
|
|
152
|
+
template: card.template,
|
|
153
|
+
state: card.state,
|
|
154
|
+
stateReason: card.stateReason,
|
|
155
|
+
lockedCount,
|
|
156
|
+
totalPhases,
|
|
157
|
+
progress: totalPhases === 0 ? 0 : lockedCount / totalPhases,
|
|
158
|
+
tampered,
|
|
159
|
+
gateBlocked: card.gateBlocked !== void 0,
|
|
160
|
+
gateKind: card.gateBlocked?.kind,
|
|
161
|
+
merged: card.mergedToRepoRoot !== void 0,
|
|
162
|
+
mergedToRepoRoot: card.mergedToRepoRoot,
|
|
163
|
+
lockValidity,
|
|
164
|
+
currentPhase,
|
|
165
|
+
subagents
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Expand a card's phase chain into full inspector rows: the always-shown
|
|
170
|
+
* '03.5' slot (§11.10 mandatory deviation) plus 01.5 when present, and
|
|
171
|
+
* 06/07/08 as three distinct rows (never one merged lane).
|
|
172
|
+
*/
|
|
173
|
+
function expandPhaseRows(card) {
|
|
174
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const [fileName, row] of Object.entries(card.phases)) {
|
|
176
|
+
const g = phaseGroupOf$1(fileName);
|
|
177
|
+
if (g !== null) byGroup.set(g, row);
|
|
178
|
+
}
|
|
179
|
+
const rows = [];
|
|
180
|
+
for (const g of GROUP_ORDER) {
|
|
181
|
+
if (g === "01.5" && !byGroup.has(g)) continue;
|
|
182
|
+
const row = byGroup.get(g);
|
|
183
|
+
if (row === void 0) {
|
|
184
|
+
if (g === "03.5") rows.push({
|
|
185
|
+
phase: g,
|
|
186
|
+
fileName: null,
|
|
187
|
+
status: "—",
|
|
188
|
+
present: false
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
rows.push({
|
|
193
|
+
phase: g,
|
|
194
|
+
fileName: row.phase,
|
|
195
|
+
status: row.status,
|
|
196
|
+
lockedAt: row.lockedAt,
|
|
197
|
+
lockHash: row.lockHash,
|
|
198
|
+
present: true
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return rows;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/client/board.tsx
|
|
205
|
+
/**
|
|
206
|
+
* Board overlay (Phase D R5, §11.8): the kanban view over the 'recursive'
|
|
207
|
+
* projection. Pure read-only renderer — no filesystem, no run-file scraping.
|
|
208
|
+
* Cards are grouped by worktree root (the projection's outer key) and mapped
|
|
209
|
+
* to their current-phase lane via derive.columnForRun.
|
|
210
|
+
*/
|
|
211
|
+
/** Flatten the worktree-grouped projection into a stable run list. */
|
|
212
|
+
function listRuns(projection) {
|
|
213
|
+
if (projection === void 0) return [];
|
|
214
|
+
const runs = [];
|
|
215
|
+
for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
|
|
216
|
+
return runs;
|
|
217
|
+
}
|
|
218
|
+
function Board({ useProjection }) {
|
|
219
|
+
const runs = listRuns(useProjection("recursive"));
|
|
220
|
+
if (runs.length === 0) return (0, react.createElement)("div", {
|
|
221
|
+
className: "rec-board",
|
|
222
|
+
"data-empty": true
|
|
223
|
+
}, (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
|
|
224
|
+
return (0, react.createElement)("div", { className: "rec-board" }, KANBAN_LANES.map((lane) => {
|
|
225
|
+
const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
|
|
226
|
+
return (0, react.createElement)("section", {
|
|
227
|
+
key: lane.id,
|
|
228
|
+
className: "rec-lane"
|
|
229
|
+
}, (0, react.createElement)("h2", { className: "rec-lane-title" }, lane.label), laneRuns.map((run) => {
|
|
230
|
+
const facts = cardFacts(run);
|
|
231
|
+
return (0, react.createElement)("article", {
|
|
232
|
+
key: run.worktreeRoot + "\0" + run.runId,
|
|
233
|
+
className: "rec-card"
|
|
234
|
+
}, (0, react.createElement)("div", { className: "rec-card-head" }, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
|
|
235
|
+
className: "rec-badge rec-badge-tamper",
|
|
236
|
+
title: "tampered"
|
|
237
|
+
}, "!")), (0, react.createElement)("div", { className: "rec-card-progress" }, (0, react.createElement)("div", {
|
|
238
|
+
className: "rec-card-progress-fill",
|
|
239
|
+
style: { width: Math.round(facts.progress * 100) + "%" }
|
|
240
|
+
})), (0, react.createElement)("div", { className: "rec-card-meta" }, (0, react.createElement)("span", {}, facts.currentPhase ?? "no phases"), (0, react.createElement)("span", { className: "rec-badge" }, facts.state)));
|
|
241
|
+
}));
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/client/strip.tsx
|
|
246
|
+
/**
|
|
247
|
+
* Status strip (Phase D R7, §11.6): a thin glance at the active run's phase,
|
|
248
|
+
* gate state, and tamper badge, docked at conversation.input.dock. Renders
|
|
249
|
+
* nothing when no run is current.
|
|
250
|
+
*/
|
|
251
|
+
function StatusStrip({ useProjection }) {
|
|
252
|
+
const runs = listRuns(useProjection("recursive"));
|
|
253
|
+
if (runs.length === 0) return null;
|
|
254
|
+
const active = runs[runs.length - 1];
|
|
255
|
+
const facts = cardFacts(active);
|
|
256
|
+
return (0, react.createElement)("div", { className: "rec-strip" }, (0, react.createElement)("span", { className: "rec-strip-run" }, active.runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.currentPhase ?? "no phases"), facts.gateBlocked && (0, react.createElement)("span", { className: "rec-badge rec-badge-gate" }, "gate blocked"), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered"));
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/client/settings.tsx
|
|
260
|
+
/**
|
|
261
|
+
* Recursive settings page (Phase D R8, §11.8): a settings.section list entry
|
|
262
|
+
* surfacing the host-owned enforcement config. The client is read-mostly; all
|
|
263
|
+
* writes ride the host config path (never run files).
|
|
264
|
+
*/
|
|
265
|
+
function RecursiveSettings({ close }) {
|
|
266
|
+
return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("h2", {}, "Recursive"), (0, react.createElement)("p", {}, "Enforcement policy, scratch format, provider defaults, and preset install path are configured on the host. The client is read-only (§11.9)."), (0, react.createElement)("button", { onClick: close }, "Close"));
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/client/slots.ts
|
|
270
|
+
/**
|
|
271
|
+
* Slot registrations (Phase D R5/R6/R7/R8, §11.8): the sidebar launcher, the
|
|
272
|
+
* shell.overlay board, the conversation.input.dock status strip, and the
|
|
273
|
+
* settings.section page. The details-seat inspector is a swap driven by the
|
|
274
|
+
* board cards' click (R6); the strip + board + settings are additive seats.
|
|
275
|
+
*
|
|
276
|
+
* Self-contained structural seam: the real slot registry + runtime standard
|
|
277
|
+
* kit arrive from @deepseek-ai/dsh-client-ui-slots + @deepseek-ai/dsh-client-runtime
|
|
278
|
+
* at compose time; this module consumes the minimal ClientContext shape
|
|
279
|
+
* (contract.ts). The client is READ-ONLY (R9) — no filesystem, no mutation.
|
|
280
|
+
*/
|
|
281
|
+
function registerSlots(ctx) {
|
|
282
|
+
const disposers = [];
|
|
283
|
+
disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
284
|
+
name: "sidebar.footer.action",
|
|
285
|
+
id: "recursive",
|
|
286
|
+
order: 50,
|
|
287
|
+
label: "Recursive runs"
|
|
288
|
+
}, () => (0, react.createElement)("button", {
|
|
289
|
+
className: "rec-launcher",
|
|
290
|
+
title: "Recursive runs"
|
|
291
|
+
}, "⧉"))));
|
|
292
|
+
disposers.push(ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
293
|
+
name: "shell.overlay",
|
|
294
|
+
id: "recursive-board",
|
|
295
|
+
order: 10
|
|
296
|
+
}, (props) => {
|
|
297
|
+
if (props?.useProjection === void 0) return null;
|
|
298
|
+
return (0, react.createElement)(Board, { useProjection: props.useProjection });
|
|
299
|
+
})));
|
|
300
|
+
disposers.push(ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
301
|
+
name: "conversation.input.dock",
|
|
302
|
+
id: "recursive-strip",
|
|
303
|
+
order: 20
|
|
304
|
+
}, (props) => {
|
|
305
|
+
if (props?.useProjection === void 0) return null;
|
|
306
|
+
return (0, react.createElement)(StatusStrip, { useProjection: props.useProjection });
|
|
307
|
+
})));
|
|
308
|
+
disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
309
|
+
name: "settings.section",
|
|
310
|
+
id: "recursive",
|
|
311
|
+
order: 90,
|
|
312
|
+
label: "Recursive"
|
|
313
|
+
}, (props) => (0, react.createElement)(RecursiveSettings, { close: props.close }))));
|
|
314
|
+
return () => {
|
|
315
|
+
for (const d of disposers) d();
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/client/node.ts
|
|
320
|
+
const RECURSIVE_NODE_KIND = "recursive/run";
|
|
321
|
+
const GROUP_INDEX = new Map([
|
|
322
|
+
"00",
|
|
323
|
+
"01",
|
|
324
|
+
"01.5",
|
|
325
|
+
"02",
|
|
326
|
+
"03",
|
|
327
|
+
"03.5",
|
|
328
|
+
"04",
|
|
329
|
+
"05",
|
|
330
|
+
"06",
|
|
331
|
+
"07",
|
|
332
|
+
"08"
|
|
333
|
+
].map((g, i) => [g, i]));
|
|
334
|
+
function phaseGroupOf(phase) {
|
|
335
|
+
const m = /^(\d{2}(?:\.\d)?)(?:-|$)/.exec(phase);
|
|
336
|
+
return m ? m[1] : null;
|
|
337
|
+
}
|
|
338
|
+
function currentPhaseOf(phases) {
|
|
339
|
+
let best = null;
|
|
340
|
+
let bestIdx = -1;
|
|
341
|
+
for (const key of Object.keys(phases)) {
|
|
342
|
+
const g = phaseGroupOf(key);
|
|
343
|
+
if (g === null) continue;
|
|
344
|
+
const idx = GROUP_INDEX.get(g) ?? -1;
|
|
345
|
+
if (idx > bestIdx) {
|
|
346
|
+
bestIdx = idx;
|
|
347
|
+
best = key;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return best;
|
|
351
|
+
}
|
|
352
|
+
function nodeDataOf(s) {
|
|
353
|
+
const lockedCount = Object.values(s.phases).filter((p) => p.status === "LOCKED").length;
|
|
354
|
+
return {
|
|
355
|
+
runId: s.runId,
|
|
356
|
+
worktreeRoot: s.worktreeRoot,
|
|
357
|
+
state: s.state,
|
|
358
|
+
currentPhase: currentPhaseOf(s.phases),
|
|
359
|
+
lockedCount,
|
|
360
|
+
totalPhases: Object.keys(s.phases).length,
|
|
361
|
+
tampered: s.tampered,
|
|
362
|
+
gateBlocked: s.gateBlocked,
|
|
363
|
+
...s.mergedToRepoRoot === void 0 ? {} : { mergedToRepoRoot: s.mergedToRepoRoot }
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** Extract a stable (runId, worktreeRoot) business id + lifecycle role from one event. */
|
|
367
|
+
function matchRecursiveNode(event) {
|
|
368
|
+
if (typeof event.type !== "string" || !event.type.startsWith("recursive/")) return null;
|
|
369
|
+
const data = event.data ?? {};
|
|
370
|
+
const runId = typeof data.runId === "string" ? data.runId : "";
|
|
371
|
+
const worktreeRoot = typeof data.worktreeRoot === "string" ? data.worktreeRoot : "";
|
|
372
|
+
if (runId === "" || worktreeRoot === "") return null;
|
|
373
|
+
return {
|
|
374
|
+
id: worktreeRoot + "\0" + runId,
|
|
375
|
+
role: event.type === "recursive/run-created" ? "start" : "update"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/** Fold one recursive/* event into the node state. */
|
|
379
|
+
function foldRecursiveNode(s, event) {
|
|
380
|
+
const data = event.data ?? {};
|
|
381
|
+
const runId = typeof data.runId === "string" ? data.runId : "";
|
|
382
|
+
const worktreeRoot = typeof data.worktreeRoot === "string" ? data.worktreeRoot : "";
|
|
383
|
+
const base = s ?? {
|
|
384
|
+
runId,
|
|
385
|
+
worktreeRoot,
|
|
386
|
+
state: "active",
|
|
387
|
+
phases: {},
|
|
388
|
+
tampered: false,
|
|
389
|
+
gateBlocked: false
|
|
390
|
+
};
|
|
391
|
+
switch (event.type) {
|
|
392
|
+
case "recursive/run-created":
|
|
393
|
+
base.runId = runId;
|
|
394
|
+
base.worktreeRoot = worktreeRoot;
|
|
395
|
+
return base;
|
|
396
|
+
case "recursive/phase": {
|
|
397
|
+
const phase = String(data.phase ?? "");
|
|
398
|
+
if (phase !== "") base.phases[phase] = {
|
|
399
|
+
phase,
|
|
400
|
+
status: String(data.status ?? "")
|
|
401
|
+
};
|
|
402
|
+
return base;
|
|
403
|
+
}
|
|
404
|
+
case "recursive/phase-locked": {
|
|
405
|
+
const phase = String(data.phase ?? "");
|
|
406
|
+
if (phase !== "") base.phases[phase] = {
|
|
407
|
+
phase,
|
|
408
|
+
status: "LOCKED"
|
|
409
|
+
};
|
|
410
|
+
return base;
|
|
411
|
+
}
|
|
412
|
+
case "recursive/gate-blocked":
|
|
413
|
+
base.gateBlocked = true;
|
|
414
|
+
return base;
|
|
415
|
+
case "recursive/tamper":
|
|
416
|
+
base.tampered = true;
|
|
417
|
+
return base;
|
|
418
|
+
case "recursive/run-state":
|
|
419
|
+
base.state = typeof data.state === "string" ? data.state : base.state;
|
|
420
|
+
return base;
|
|
421
|
+
case "recursive/run-merged": {
|
|
422
|
+
const repoRoot = typeof data.repoRoot === "string" ? data.repoRoot : "";
|
|
423
|
+
if (repoRoot !== "") {
|
|
424
|
+
base.mergedToRepoRoot = repoRoot;
|
|
425
|
+
base.worktreeRoot = repoRoot;
|
|
426
|
+
}
|
|
427
|
+
return base;
|
|
428
|
+
}
|
|
429
|
+
default: return base;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** The structural ConversationNodeDefinition (host links to the real contract). */
|
|
433
|
+
const recursiveNodeDefinition = {
|
|
434
|
+
kind: RECURSIVE_NODE_KIND,
|
|
435
|
+
target: "chat",
|
|
436
|
+
match: matchRecursiveNode,
|
|
437
|
+
start: (_ctx, match) => foldRecursiveNode(void 0, match.event),
|
|
438
|
+
update: (ctx, match) => foldRecursiveNode(ctx.state, match.event),
|
|
439
|
+
buildViewNode: (ctx) => {
|
|
440
|
+
const state = ctx.state;
|
|
441
|
+
if (state === void 0) return null;
|
|
442
|
+
return {
|
|
443
|
+
key: ctx.key,
|
|
444
|
+
kind: RECURSIVE_NODE_KIND,
|
|
445
|
+
id: ctx.id,
|
|
446
|
+
target: "chat",
|
|
447
|
+
data: nodeDataOf(state)
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/client/inspector.tsx
|
|
453
|
+
/**
|
|
454
|
+
* Drill-down inspector (Phase D R6, §11.5): the details-seat swap rendering
|
|
455
|
+
* one run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows),
|
|
456
|
+
* lock badges, and tamper/gate facts — all from the projection, never disk.
|
|
457
|
+
*/
|
|
458
|
+
function Inspector({ runId, worktreeRoot, useProjection, onBackToToolDetails }) {
|
|
459
|
+
const card = useProjection("recursive")?.[worktreeRoot]?.[runId];
|
|
460
|
+
if (card === void 0) return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("p", {}, "Run not found: " + runId));
|
|
461
|
+
const facts = cardFacts(card);
|
|
462
|
+
const rows = expandPhaseRows(card);
|
|
463
|
+
return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-inspector-head" }, (0, react.createElement)("button", {
|
|
464
|
+
onClick: onBackToToolDetails,
|
|
465
|
+
className: "rec-back"
|
|
466
|
+
}, "← back to tool details"), (0, react.createElement)("h2", {}, runId), (0, react.createElement)("span", { className: "rec-badge" }, facts.state), facts.tampered && (0, react.createElement)("span", { className: "rec-badge rec-badge-tamper" }, "tampered")), (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
|
|
467
|
+
key: row.phase,
|
|
468
|
+
className: "rec-phase-row"
|
|
469
|
+
}, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", { className: "rec-badge" }, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", { className: "rec-inspector-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
|
|
470
|
+
}
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/client/index.ts
|
|
473
|
+
/** Browser-half plugin entry (R4). */
|
|
474
|
+
function apply(ctx) {
|
|
475
|
+
registerSlots(ctx);
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
exports.Board = Board;
|
|
479
|
+
exports.Inspector = Inspector;
|
|
480
|
+
exports.RECURSIVE_NODE_KIND = RECURSIVE_NODE_KIND;
|
|
481
|
+
exports.RecursiveSettings = RecursiveSettings;
|
|
482
|
+
exports.StatusStrip = StatusStrip;
|
|
483
|
+
exports.apply = apply;
|
|
484
|
+
exports.foldRecursiveNode = foldRecursiveNode;
|
|
485
|
+
exports.listRuns = listRuns;
|
|
486
|
+
exports.matchRecursiveNode = matchRecursiveNode;
|
|
487
|
+
exports.recursiveNodeDefinition = recursiveNodeDefinition;
|
|
488
|
+
return module.exports;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CloseoutPhaseConfig {
|
|
2
|
+
file: string;
|
|
3
|
+
label: string;
|
|
4
|
+
scopeNote: string;
|
|
5
|
+
todoItems: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const PHASE_CONFIG: Record<string, CloseoutPhaseConfig>;
|
|
8
|
+
export interface CloseoutResult {
|
|
9
|
+
phase: string;
|
|
10
|
+
file: string;
|
|
11
|
+
created: string[];
|
|
12
|
+
existing: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface CloseoutOptions {
|
|
15
|
+
/** Enforce prerequisite-lock gating (default true). */
|
|
16
|
+
strict?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create or update a closeout receipt stub for the given phase.
|
|
20
|
+
* Returns the file + created/existing lists. When strict, refuses phases whose
|
|
21
|
+
* prerequisite artifacts are not all LOCKED (reuses lock.ts chain validation).
|
|
22
|
+
*/
|
|
23
|
+
export declare function closeoutPhase(runDir: string, phase: string, opts?: CloseoutOptions): CloseoutResult;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RecursiveRuntime } from './runtime.ts';
|
|
2
|
+
export type RecursiveVerb = 'status' | 'spec' | 'worktree' | 'init' | 'lock' | 'qa' | 'closeout' | 'addendum' | 'review' | 'scratch' | 'bootstrap' | 'list' | 'help';
|
|
3
|
+
export declare const PRESET_VERBS: RecursiveVerb[];
|
|
4
|
+
export declare const GLOBAL_VERBS: RecursiveVerb[];
|
|
5
|
+
export declare const ALL_VERBS: RecursiveVerb[];
|
|
6
|
+
export interface ParsedRecursiveCommand {
|
|
7
|
+
verb: string;
|
|
8
|
+
arg: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseRecursiveCommand(rawInput: string): ParsedRecursiveCommand;
|
|
11
|
+
export type RecursiveCommandResult = {
|
|
12
|
+
kind: 'success';
|
|
13
|
+
text?: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'error';
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Execute a /recursive command line against a workspace root (the session's
|
|
20
|
+
* control-plane root). Pure + workspace-scoped: runId outside root errors.
|
|
21
|
+
*/
|
|
22
|
+
export declare function executeRecursiveCommand(root: string, rawInput: string): RecursiveCommandResult;
|
|
23
|
+
export interface RecursiveCommandDefinition {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
input: {
|
|
27
|
+
hint: string;
|
|
28
|
+
};
|
|
29
|
+
handler: (inv: RecursiveCommandInvocation) => RecursiveCommandResult | Promise<RecursiveCommandResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface RecursiveCommandInvocation {
|
|
32
|
+
rawInput: string;
|
|
33
|
+
agent?: {
|
|
34
|
+
session?: {
|
|
35
|
+
header?: {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
export interface RecursiveCommandRuntime {
|
|
42
|
+
register(def: RecursiveCommandDefinition): () => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register the /recursive command on a context with commands (Stage A).
|
|
46
|
+
* The handler resolves the session workspace root via the agent's cwd and
|
|
47
|
+
* executes scoped to that root only (R1).
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerRecursiveCommand(ctx: {
|
|
50
|
+
commands: RecursiveCommandRuntime;
|
|
51
|
+
}, recursive: RecursiveRuntime): () => void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Minimal host-realm contract for ctx.subagents (the seam we call). */
|
|
2
|
+
export interface SubagentsRuntimeLike {
|
|
3
|
+
start(name: string, request: SubagentStartRequestLike): Promise<SubagentResultLike>;
|
|
4
|
+
getProvider?(name: string): unknown;
|
|
5
|
+
list?(): unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface SubagentStartRequestLike {
|
|
8
|
+
prompt: unknown[];
|
|
9
|
+
label?: string;
|
|
10
|
+
outputSchema?: Record<string, unknown>;
|
|
11
|
+
toolFilter?: unknown;
|
|
12
|
+
maxDepth?: number;
|
|
13
|
+
persona?: string;
|
|
14
|
+
parent?: unknown;
|
|
15
|
+
signal?: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface SubagentResultLike {
|
|
18
|
+
output?: string;
|
|
19
|
+
structured?: unknown;
|
|
20
|
+
stopReason?: string;
|
|
21
|
+
success?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface DelegationError extends Error {
|
|
24
|
+
code?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function delegationError(message: string, code: string): DelegationError;
|
|
27
|
+
/** Object-rooted output schema for a delegated review. */
|
|
28
|
+
export declare function reviewOutputSchema(): Record<string, unknown>;
|
|
29
|
+
/** Default toolFilter for a delegated reviewer (run-relevant, workspace-scoped). */
|
|
30
|
+
export declare function defaultReviewToolFilter(): unknown;
|
|
31
|
+
/**
|
|
32
|
+
* Call ctx.subagents.start() with the full request. Fail loud on capability
|
|
33
|
+
* mismatch, missing provider, or unsupported schema (never silent).
|
|
34
|
+
*/
|
|
35
|
+
export declare function delegate(input: {
|
|
36
|
+
subagents: SubagentsRuntimeLike;
|
|
37
|
+
provider: string;
|
|
38
|
+
request: SubagentStartRequestLike;
|
|
39
|
+
}): Promise<SubagentResultLike>;
|
|
40
|
+
export interface Reference {
|
|
41
|
+
path: string;
|
|
42
|
+
lineRange?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ReferenceCheck {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
failures: string[];
|
|
47
|
+
checked: {
|
|
48
|
+
path: string;
|
|
49
|
+
ok: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate every claimed reference: the path exists under root, and a line
|
|
55
|
+
* range (e.g. '10-20' or '10') is within the file's line count.
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateReferences(root: string, references: Reference[]): ReferenceCheck;
|
|
58
|
+
export interface ActionRecordInput {
|
|
59
|
+
root: string;
|
|
60
|
+
runId: string;
|
|
61
|
+
subagentId: string;
|
|
62
|
+
phase: string;
|
|
63
|
+
purpose: string;
|
|
64
|
+
executionMode: string;
|
|
65
|
+
artifactPath?: string;
|
|
66
|
+
upstreamArtifacts?: string[];
|
|
67
|
+
reviewBundle?: string;
|
|
68
|
+
diffBasis?: string;
|
|
69
|
+
codeRefs?: string[];
|
|
70
|
+
auditQuestions?: string[];
|
|
71
|
+
actionsTaken?: string[];
|
|
72
|
+
createdFiles?: string[];
|
|
73
|
+
modifiedFiles?: string[];
|
|
74
|
+
reviewedFiles?: string[];
|
|
75
|
+
findings?: string[];
|
|
76
|
+
success: boolean;
|
|
77
|
+
stopReason?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write a durable action record under subagents/ with the canonical sections
|
|
81
|
+
* (matches recursive-subagent-action.py). A success:false attempt is written
|
|
82
|
+
* with a failed status and is NOT accepted.
|
|
83
|
+
*/
|
|
84
|
+
export declare function writeActionRecord(input: ActionRecordInput): string;
|
|
85
|
+
/**
|
|
86
|
+
* Accept/reject a delegation result: a success:false or non-completed
|
|
87
|
+
* stopReason is a failed attempt (diagnostics preserved, NOT accepted).
|
|
88
|
+
*/
|
|
89
|
+
export declare function evaluateDelegationResult(result: SubagentResultLike): {
|
|
90
|
+
accepted: boolean;
|
|
91
|
+
reason: string;
|
|
92
|
+
};
|