@ordewell/core 0.4.22 → 0.5.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/dist/{ITerminalRunner-CuNYBnqK.d.mts → ITerminalRunner-DXlJop_f.d.mts} +26 -1
- package/dist/{ITerminalRunner-Da1gIAFX.d.ts → ITerminalRunner-nupxhtPb.d.ts} +26 -1
- package/dist/{ModeResolver-Ob-ihQbI.d.ts → ModeResolver-BBVktyiP.d.ts} +1 -1
- package/dist/{ModeResolver-mtIWrLXn.d.mts → ModeResolver-D1-2xJsE.d.mts} +1 -1
- package/dist/Task-CfTjRQeS.d.mts +586 -0
- package/dist/Task-CfTjRQeS.d.ts +586 -0
- package/dist/{chunk-DV3YIKRA.mjs → chunk-GWPIYDQW.mjs} +31 -7
- package/dist/chunk-GWPIYDQW.mjs.map +1 -0
- package/dist/chunk-HQICT7HQ.mjs +90 -0
- package/dist/chunk-HQICT7HQ.mjs.map +1 -0
- package/dist/{chunk-T4KGVP4N.mjs → chunk-JVMDEHRQ.mjs} +1 -1
- package/dist/chunk-JVMDEHRQ.mjs.map +1 -0
- package/dist/{chunk-MKUTJDT2.mjs → chunk-XWOUIA6A.mjs} +2 -2
- package/dist/index.d.mts +606 -107
- package/dist/index.d.ts +606 -107
- package/dist/index.js +3357 -1172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3347 -1264
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +1 -1
- package/dist/order-labels.d.ts +1 -1
- package/dist/{parsing-BWfpLch9.d.mts → parsing-BJECTMOD.d.mts} +2 -2
- package/dist/{parsing-BJoHbA-t.d.ts → parsing-Bv3g2Tu_.d.ts} +2 -2
- package/dist/parsing.d.mts +3 -3
- package/dist/parsing.d.ts +3 -3
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-s6VL8NDF.d.mts → plan-utils-B5oIZkuI.d.mts} +30 -5
- package/dist/{plan-utils-DJm2a7Zp.d.ts → plan-utils-Cy-RsBSV.d.ts} +30 -5
- package/dist/plan-utils.d.mts +3 -3
- package/dist/plan-utils.d.ts +3 -3
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +2 -2
- package/dist/testing.d.mts +81 -3
- package/dist/testing.d.ts +81 -3
- package/dist/testing.js +152 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +136 -0
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/Task-4rDGr8Su.d.mts +0 -271
- package/dist/Task-4rDGr8Su.d.ts +0 -271
- package/dist/chunk-DV3YIKRA.mjs.map +0 -1
- package/dist/chunk-T4KGVP4N.mjs.map +0 -1
- /package/dist/{chunk-MKUTJDT2.mjs.map → chunk-XWOUIA6A.mjs.map} +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why isolated execution is unavailable for a workspace. The orchestrator needs
|
|
3
|
+
* the reason, not just a boolean: `dirty` is offered a stash or an explicit
|
|
4
|
+
* "run without isolation", while the others fall back to the shared
|
|
5
|
+
* workspace root with a one-line notice.
|
|
6
|
+
*/
|
|
7
|
+
type IsolationInactiveReason = 'disabled' | 'git-missing' | 'not-git' | 'no-commits' | 'dirty' | 'nested-repos';
|
|
8
|
+
/**
|
|
9
|
+
* `repos` names, relative to the workspace, the repositories behind the answer:
|
|
10
|
+
* when active, the ones that will isolate, with `shared` the paths every task
|
|
11
|
+
* will share live; otherwise the nested ones `nested-repos` refuses, the dirty
|
|
12
|
+
* ones of a `dirty` group, or the commitless ones of a `no-commits` group. A
|
|
13
|
+
* group of one names none.
|
|
14
|
+
*/
|
|
15
|
+
type IsolationAvailability = {
|
|
16
|
+
active: true;
|
|
17
|
+
repos?: string[];
|
|
18
|
+
shared?: string[];
|
|
19
|
+
} | {
|
|
20
|
+
active: false;
|
|
21
|
+
reason: IsolationInactiveReason;
|
|
22
|
+
repos?: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Where a run's tasks work, as the planner is told it: the repos of the group
|
|
26
|
+
* and the paths shared live between tasks. A lone repository is `['.']` with
|
|
27
|
+
* nothing shared.
|
|
28
|
+
*/
|
|
29
|
+
interface RepoGroupLayout {
|
|
30
|
+
repos: string[];
|
|
31
|
+
shared: string[];
|
|
32
|
+
}
|
|
33
|
+
type IsolationOutcome = 'merged' | 'conflict' | 'failed';
|
|
34
|
+
/**
|
|
35
|
+
* `active` — worktree exists, a runner may be writing to it.
|
|
36
|
+
* `kept` — released with its worktree and branch preserved for inspection.
|
|
37
|
+
* `conflict` — integration stopped on a merge conflict; worktree and refs kept.
|
|
38
|
+
* `failed` — integration hit a git error other than a conflict; refs kept.
|
|
39
|
+
* `merged` — landed on the integration branch; worktree and task branch removed.
|
|
40
|
+
*/
|
|
41
|
+
type IsolationTaskStatus = 'active' | 'kept' | 'conflict' | 'failed' | 'merged';
|
|
42
|
+
/** One repo's share of a task: its worktree inside the task workspace. */
|
|
43
|
+
interface IsolationTaskRepo {
|
|
44
|
+
/** Absolute path of this repo's worktree: the task workspace joined with the repo's path. */
|
|
45
|
+
worktree: string;
|
|
46
|
+
/**
|
|
47
|
+
* Paths bootstrapped from the real repo (symlinks, junctions, copies).
|
|
48
|
+
* Recorded so the commit step can leave them out — a symlink is not matched
|
|
49
|
+
* by a `node_modules/` ignore rule and would otherwise be committed.
|
|
50
|
+
*/
|
|
51
|
+
linked: string[];
|
|
52
|
+
/** Whether the task brought commits to this repo; unknown until it first integrates. */
|
|
53
|
+
changed?: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface IsolationTaskRecord {
|
|
56
|
+
taskId: string;
|
|
57
|
+
order: number;
|
|
58
|
+
title: string;
|
|
59
|
+
/** One branch name, the same in every repo, so a task is one name to look up across the group. */
|
|
60
|
+
branch: string;
|
|
61
|
+
/**
|
|
62
|
+
* Absolute path of the task workspace, holding one worktree per repo at the
|
|
63
|
+
* repo's path. The Runner's cwd is inside it when the workspace is a repo
|
|
64
|
+
* subdirectory; for a group of one it is the worktree.
|
|
65
|
+
*/
|
|
66
|
+
workspace: string;
|
|
67
|
+
/** For the task as a whole: landing is atomic across the repos it changed. */
|
|
68
|
+
status: IsolationTaskStatus;
|
|
69
|
+
/** Keyed by repo path. */
|
|
70
|
+
repos: Record<string, IsolationTaskRepo>;
|
|
71
|
+
/** The repo whose merge stopped the task from landing, while `status` is `conflict` or `failed`. */
|
|
72
|
+
conflictRepo?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A task's landing in flight: each changed repo's integration tip from before
|
|
76
|
+
* the task's merge. On the run rather than the task record because it must
|
|
77
|
+
* outlive that record — a retry drops and recreates it — until every repo is
|
|
78
|
+
* back at its tip or the task has landed.
|
|
79
|
+
*/
|
|
80
|
+
interface IsolationLanding {
|
|
81
|
+
taskId: string;
|
|
82
|
+
/** Keyed by repo path. */
|
|
83
|
+
tips: Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* One repository of the group (ADR-0014). Every git operation on it runs in
|
|
87
|
+
* `root`, never in the workspace root.
|
|
88
|
+
*/
|
|
89
|
+
interface IsolationRepo {
|
|
90
|
+
/** Relative to the workspace root; `.` when the workspace is itself the repository. */
|
|
91
|
+
path: string;
|
|
92
|
+
/**
|
|
93
|
+
* Absolute: the workspace root joined with `path`. For a group of one that is
|
|
94
|
+
* the workspace root, which may be a subdirectory of the repository.
|
|
95
|
+
*/
|
|
96
|
+
root: string;
|
|
97
|
+
/** The commit checked out at run start. Switching branches mid-run does not retarget it. */
|
|
98
|
+
baseRef: string;
|
|
99
|
+
/** Branch name checked out at run start; absent on a detached HEAD. */
|
|
100
|
+
baseBranch?: string;
|
|
101
|
+
integrationBranch: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One Execute-Plan click or one manual task run over the workspace's repo
|
|
105
|
+
* group. Plain JSON on purpose: the orchestrator persists it with the plan
|
|
106
|
+
* state so a resumed session can find its integration branches again. The
|
|
107
|
+
* module mutates `tasks` in place.
|
|
108
|
+
*/
|
|
109
|
+
interface IsolationRun {
|
|
110
|
+
id: string;
|
|
111
|
+
workspaceRoot: string;
|
|
112
|
+
repos: IsolationRepo[];
|
|
113
|
+
/**
|
|
114
|
+
* Workspace paths outside every isolated repo, linked live into each task
|
|
115
|
+
* workspace: loose entries of the workspace root, the entries beside a deeper
|
|
116
|
+
* repo, and `sharedRepos`. Empty for a group of one.
|
|
117
|
+
*/
|
|
118
|
+
shared: string[];
|
|
119
|
+
/** Repos of the group that could not be isolated — no commits, or git refused a worktree — and are among `shared`. */
|
|
120
|
+
sharedRepos: string[];
|
|
121
|
+
/** Keyed by task id — ids are unique within one plan and a run belongs to one plan. */
|
|
122
|
+
tasks: Record<string, IsolationTaskRecord>;
|
|
123
|
+
/**
|
|
124
|
+
* Set before a task's first merge and cleared once it has landed or been
|
|
125
|
+
* rolled back. One found set — after a crash, or a rollback git refused —
|
|
126
|
+
* names exactly what to return each repo's integration branch to.
|
|
127
|
+
*/
|
|
128
|
+
landing?: IsolationLanding;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What a plan persists of isolated execution (`LegacyPlanState.isolation`): its
|
|
132
|
+
* run, and which added tasks resolve which conflicts. Belongs to that plan and
|
|
133
|
+
* its branches alone, so a copy of the plan (a fork) must not carry it.
|
|
134
|
+
*/
|
|
135
|
+
interface PlanIsolation {
|
|
136
|
+
run: IsolationRun;
|
|
137
|
+
/** Resolver task id → the conflicted task whose branch it merges. */
|
|
138
|
+
resolvers: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A task's isolation as a surface shows it. `kept` covers every record whose
|
|
142
|
+
* worktree stays for inspection — a failed verdict, an interrupted attempt, an
|
|
143
|
+
* integration git refused — because to the user they are one thing: work that
|
|
144
|
+
* did not land and can be looked at. `none` is a task with no worktree in a plan
|
|
145
|
+
* that has an isolation run.
|
|
146
|
+
*/
|
|
147
|
+
type TaskIsolationState = 'none' | 'active' | 'integrated' | 'conflict' | 'kept';
|
|
148
|
+
type TaskIsolation = {
|
|
149
|
+
state: 'none';
|
|
150
|
+
} | {
|
|
151
|
+
state: Exclude<TaskIsolationState, 'none'>;
|
|
152
|
+
branch: string;
|
|
153
|
+
/** The task workspace; for a group of one, the task's worktree. */
|
|
154
|
+
worktree: string;
|
|
155
|
+
/** Paths of the repos the task changed. */
|
|
156
|
+
repos: string[];
|
|
157
|
+
conflictRepo?: string;
|
|
158
|
+
};
|
|
159
|
+
interface IsolationLandedTask {
|
|
160
|
+
taskId: string;
|
|
161
|
+
order: number;
|
|
162
|
+
title: string;
|
|
163
|
+
}
|
|
164
|
+
interface IsolationHandoffRepo {
|
|
165
|
+
path: string;
|
|
166
|
+
integrationBranch: string;
|
|
167
|
+
baseRef: string;
|
|
168
|
+
/** Tasks whose work landed in this repo, in plan order. */
|
|
169
|
+
landed: IsolationLandedTask[];
|
|
170
|
+
}
|
|
171
|
+
interface IsolationHandoff {
|
|
172
|
+
repos: IsolationHandoffRepo[];
|
|
173
|
+
/** Tasks that landed on the integration branches, in plan order. */
|
|
174
|
+
landed: IsolationLandedTask[];
|
|
175
|
+
}
|
|
176
|
+
/** A plan's isolation as a surface shows it: a mark for each task the run touched, and its handoff. */
|
|
177
|
+
interface IsolationView {
|
|
178
|
+
tasks: Record<string, TaskIsolation>;
|
|
179
|
+
handoff: IsolationHandoff;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Why "Merge all" would not touch a repo. `partial-landing`: a task's landing
|
|
183
|
+
* was interrupted and could not be rolled back there, so its integration
|
|
184
|
+
* branch holds part of a task.
|
|
185
|
+
*/
|
|
186
|
+
type IsolationMergeBlockReason = 'merge-in-progress' | 'conflict' | 'uncommitted-changes' | 'partial-landing' | 'git-error';
|
|
187
|
+
interface IsolationMergeBlock {
|
|
188
|
+
repo: string;
|
|
189
|
+
reason: IsolationMergeBlockReason;
|
|
190
|
+
/** The files that would conflict, or the user's uncommitted ones the merge also changes; empty for the other reasons. */
|
|
191
|
+
files: string[];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* How "Merge all" went.
|
|
195
|
+
* - `merged`: every repo with work on its integration branch took it.
|
|
196
|
+
* - `blocked`: the preflight found repos that could not, so nothing was
|
|
197
|
+
* touched anywhere; `blocked` says which and why.
|
|
198
|
+
* - `conflict` / `failed`: a merge stopped in `repo` — on git older than 2.38,
|
|
199
|
+
* which cannot preflight, or for a reason no preflight could foresee. That
|
|
200
|
+
* merge was aborted, leaving `repo` as it was; `landed` names the repos
|
|
201
|
+
* merged before it, which stay merged, and is absent when there are none.
|
|
202
|
+
*
|
|
203
|
+
* A group of one is blocked only by a partial landing; otherwise its one merge
|
|
204
|
+
* lands or is aborted whole, so it reports as it always has.
|
|
205
|
+
*/
|
|
206
|
+
type IsolationMergeResult = {
|
|
207
|
+
outcome: 'merged';
|
|
208
|
+
} | {
|
|
209
|
+
outcome: 'blocked';
|
|
210
|
+
blocked: IsolationMergeBlock[];
|
|
211
|
+
} | {
|
|
212
|
+
outcome: 'conflict' | 'failed';
|
|
213
|
+
repo: string;
|
|
214
|
+
files?: string[];
|
|
215
|
+
landed?: string[];
|
|
216
|
+
};
|
|
217
|
+
interface PreparedTask {
|
|
218
|
+
cwd: string;
|
|
219
|
+
branch: string;
|
|
220
|
+
/**
|
|
221
|
+
* Paths, relative to the task workspace, that are copies rather than links
|
|
222
|
+
* because a hard link was impossible (Windows, another volume). Edits to them
|
|
223
|
+
* stay in the task, so the user is told.
|
|
224
|
+
*/
|
|
225
|
+
copied: string[];
|
|
226
|
+
}
|
|
227
|
+
interface IWorktreeIsolation {
|
|
228
|
+
/**
|
|
229
|
+
* A repo group with at least one repo to isolate, a clean tracked tree in
|
|
230
|
+
* each, and the config enabled; otherwise the reason it is not.
|
|
231
|
+
*/
|
|
232
|
+
isActive(workspaceRoot: string): Promise<IsolationAvailability>;
|
|
233
|
+
/**
|
|
234
|
+
* Put the tracked changes of every dirty repo of the group on its git stash,
|
|
235
|
+
* the user's way out of a `dirty` refusal. Untracked files stay: they never
|
|
236
|
+
* block isolation.
|
|
237
|
+
*/
|
|
238
|
+
stash(workspaceRoot: string): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* Mint a run: resolve each repo's base ref to a commit now, and share the
|
|
241
|
+
* repos that cannot be isolated. Only meaningful after `isActive` said yes;
|
|
242
|
+
* throws when no repo of the group can be isolated after all.
|
|
243
|
+
*/
|
|
244
|
+
startRun(workspaceRoot: string): Promise<IsolationRun>;
|
|
245
|
+
/**
|
|
246
|
+
* Create the task workspace — one worktree per isolated repo from its
|
|
247
|
+
* integration tip, the shared paths linked in — and return the cwd to spawn
|
|
248
|
+
* the Runner into. A second `prepare` for the same task is a retry: the old
|
|
249
|
+
* attempt is discarded and the workspace recreated from the tips, so the
|
|
250
|
+
* task sees everything its predecessors have integrated.
|
|
251
|
+
*/
|
|
252
|
+
prepare(task: Task, run: IsolationRun): Promise<PreparedTask>;
|
|
253
|
+
/**
|
|
254
|
+
* Land the task atomically across the repos it changed: commit each
|
|
255
|
+
* worktree, then `git merge --no-ff` the task branch into each changed
|
|
256
|
+
* repo's integration branch. If any merge conflicts or fails, it is aborted
|
|
257
|
+
* and the merges already made for the task are reset away, so `merged`
|
|
258
|
+
* always means the whole task landed. Serialized inside the module; among
|
|
259
|
+
* tasks waiting at once the lowest plan order goes first. On anything but
|
|
260
|
+
* `merged` the worktrees and refs stay, and nothing is ever auto-resolved.
|
|
261
|
+
*
|
|
262
|
+
* `persist` is called once `run.landing` is set and before the first
|
|
263
|
+
* merge; the caller saves the run there, synchronously, which is what
|
|
264
|
+
* lets `pruneOrphans` finish a landing a crash interrupted.
|
|
265
|
+
*/
|
|
266
|
+
integrate(task: Task, run: IsolationRun, persist?: () => void): Promise<IsolationOutcome>;
|
|
267
|
+
/**
|
|
268
|
+
* `keep: false` removes the task's worktree, branch and record (cancel, task
|
|
269
|
+
* removal). `keep: true` leaves the worktree and branch exactly as they are
|
|
270
|
+
* for inspection — a failed verdict — and only moves the task off `active`,
|
|
271
|
+
* so a crash-recovery prune does not sweep it away. Takes the run rather than
|
|
272
|
+
* a bare task id: ids are only unique within one plan, and one daemon serves
|
|
273
|
+
* many (ADR-0007).
|
|
274
|
+
*/
|
|
275
|
+
release(run: IsolationRun, taskId: string, opts: {
|
|
276
|
+
keep: boolean;
|
|
277
|
+
}): Promise<void>;
|
|
278
|
+
/** End of run: park the integration branch for review and report what landed. */
|
|
279
|
+
handoff(run: IsolationRun): Promise<IsolationHandoff>;
|
|
280
|
+
/**
|
|
281
|
+
* Drop what a crash left behind: a landing it interrupted is rolled back in
|
|
282
|
+
* every repo, then stale active worktrees and directories no record owns go.
|
|
283
|
+
*/
|
|
284
|
+
pruneOrphans(run: IsolationRun): Promise<void>;
|
|
285
|
+
/** Unified diff of each repo's integration branch against its base ref. */
|
|
286
|
+
reviewDiff(run: IsolationRun): Promise<string>;
|
|
287
|
+
/**
|
|
288
|
+
* "Merge all": merge each repo's integration branch into whatever the user
|
|
289
|
+
* has checked out there. The one irreversible step, so it only ever happens
|
|
290
|
+
* when a caller asks for it. Every repo with work is preflighted first — no
|
|
291
|
+
* merge of the user's in progress, no conflict against their HEAD, no
|
|
292
|
+
* uncommitted edit to a file the merge changes — and unless all pass,
|
|
293
|
+
* nothing is merged anywhere. Only a merge Ordewell itself just started is
|
|
294
|
+
* ever aborted; nothing of the user's is reset.
|
|
295
|
+
*/
|
|
296
|
+
mergeIntoCheckedOut(run: IsolationRun): Promise<IsolationMergeResult>;
|
|
297
|
+
/**
|
|
298
|
+
* Remove every worktree and task branch of the run, and the integration
|
|
299
|
+
* branch too unless `keepIntegration` — the branch outlives a discarded run
|
|
300
|
+
* until the user explicitly gives it up.
|
|
301
|
+
*/
|
|
302
|
+
discard(run: IsolationRun, opts: {
|
|
303
|
+
keepIntegration: boolean;
|
|
304
|
+
}): Promise<void>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface UserStep {
|
|
308
|
+
order: number;
|
|
309
|
+
instruction: string;
|
|
310
|
+
completed: boolean;
|
|
311
|
+
}
|
|
312
|
+
/** One deterministic signal gathered while verifying a completed task. */
|
|
313
|
+
interface VerificationCheck {
|
|
314
|
+
name: 'exit_code' | 'completion_marker' | 'manual';
|
|
315
|
+
passed: boolean;
|
|
316
|
+
/** A check that did not apply. Skipped checks don't affect the verdict. */
|
|
317
|
+
skipped: boolean;
|
|
318
|
+
detail: string;
|
|
319
|
+
}
|
|
320
|
+
/** Evidence-based verdict for a completed task. Single end-to-end outcome produced by verification. */
|
|
321
|
+
interface Verdict {
|
|
322
|
+
outcome: 'pass' | 'fail';
|
|
323
|
+
reason: string;
|
|
324
|
+
checks: VerificationCheck[];
|
|
325
|
+
decidedAt: string;
|
|
326
|
+
}
|
|
327
|
+
interface TaskOutputSummary {
|
|
328
|
+
reviewReason: string;
|
|
329
|
+
logTail: string;
|
|
330
|
+
capturedAt: string;
|
|
331
|
+
}
|
|
332
|
+
type TaskType = 'ai' | 'user';
|
|
333
|
+
type TaskStatus = 'pending' | 'approved' | 'in_progress' | 'completed' | 'failed' | 'blocked' | 'awaiting_user';
|
|
334
|
+
type TaskMode = string;
|
|
335
|
+
interface TaskModelAssignment {
|
|
336
|
+
modelId: string;
|
|
337
|
+
modelLabel: string;
|
|
338
|
+
thinkingEffort?: string;
|
|
339
|
+
/**
|
|
340
|
+
* All variant ids the model offered when this assignment was made. Carried
|
|
341
|
+
* on the assignment because runners need it at spawn time (opencode's TUI
|
|
342
|
+
* only honors an assigned variant when the others are config-disabled) and
|
|
343
|
+
* the discovery catalog isn't available there.
|
|
344
|
+
*/
|
|
345
|
+
availableVariants?: string[];
|
|
346
|
+
}
|
|
347
|
+
type RunnerId = string;
|
|
348
|
+
interface Task {
|
|
349
|
+
id: string;
|
|
350
|
+
order: number;
|
|
351
|
+
title: string;
|
|
352
|
+
description: string;
|
|
353
|
+
type: TaskType;
|
|
354
|
+
status: TaskStatus;
|
|
355
|
+
dependencies: string[];
|
|
356
|
+
prompt?: string;
|
|
357
|
+
userSteps?: UserStep[];
|
|
358
|
+
subtasks: Task[];
|
|
359
|
+
verdict?: Verdict;
|
|
360
|
+
outputSummary?: TaskOutputSummary;
|
|
361
|
+
assignedModel?: TaskModelAssignment;
|
|
362
|
+
assignedRunner: RunnerId;
|
|
363
|
+
thinkingEffort?: string;
|
|
364
|
+
taskMode?: TaskMode;
|
|
365
|
+
completionMarker: string;
|
|
366
|
+
autonomy?: 'AFK' | 'HITL';
|
|
367
|
+
sliceType?: 'HITL' | 'AFK';
|
|
368
|
+
userStoriesCovered?: string[];
|
|
369
|
+
}
|
|
370
|
+
interface DiscoveredMode {
|
|
371
|
+
id: string;
|
|
372
|
+
label: string;
|
|
373
|
+
description: string;
|
|
374
|
+
}
|
|
375
|
+
type ResearchToolType = 'read_file' | 'read_files' | 'glob' | 'grep' | 'find_symbol' | 'list_dir' | 'bash' | 'fetch' | 'web_search' | 'spawn_research_agent'
|
|
376
|
+
/**
|
|
377
|
+
* A tool belonging to a harness planner's own toolbox (ADR-0009) that has no
|
|
378
|
+
* Ordewell equivalent — Edit, WebFetch, TodoWrite, whatever a coding agent
|
|
379
|
+
* ships next. The real name travels in `toolLabel` rather than being
|
|
380
|
+
* relabelled as a tool it is not; the union stays closed so the
|
|
381
|
+
* exhaustiveness checks in every surface's icon/label switch survive.
|
|
382
|
+
*/
|
|
383
|
+
| 'agent_tool';
|
|
384
|
+
/**
|
|
385
|
+
* What happened when a research tool call ran, for honest per-surface
|
|
386
|
+
* rendering. The broadcast seam carries this on every `research_step_done` so
|
|
387
|
+
* surfaces do not have to pattern-match refusal text to tell a refused `rm`
|
|
388
|
+
* from a successful `rm` — the old render path flipped a `✓` for both.
|
|
389
|
+
*/
|
|
390
|
+
type ResearchStepOutcome = 'success' | 'failure' | 'refused' | 'denied' | 'not_executed';
|
|
391
|
+
interface ResearchStep {
|
|
392
|
+
id: string;
|
|
393
|
+
tool: ResearchToolType;
|
|
394
|
+
/** The tool's own name when it came from a harness planner — always set for `agent_tool`. */
|
|
395
|
+
toolLabel?: string;
|
|
396
|
+
args: string;
|
|
397
|
+
result: string;
|
|
398
|
+
success: boolean;
|
|
399
|
+
outcome: ResearchStepOutcome;
|
|
400
|
+
/** The model's tool_call id, so a surface can match `tool_result` to the
|
|
401
|
+
* pending `tool_call` it announced — robust under parallel same-tool rounds. */
|
|
402
|
+
toolCallId?: string;
|
|
403
|
+
timestamp: string;
|
|
404
|
+
thinkingText?: string;
|
|
405
|
+
}
|
|
406
|
+
interface UserPromptEntry {
|
|
407
|
+
id: string;
|
|
408
|
+
type: 'user_prompt' | 'system';
|
|
409
|
+
content: string;
|
|
410
|
+
timestamp: string;
|
|
411
|
+
}
|
|
412
|
+
type ResearchLogEntry = ResearchStep | UserPromptEntry;
|
|
413
|
+
interface ResearchProgress {
|
|
414
|
+
type: 'thinking' | 'tool_call' | 'tool_result' | 'plan_token' | 'interrupted' | 'liveness';
|
|
415
|
+
text?: string;
|
|
416
|
+
tool?: string;
|
|
417
|
+
/** Harness planners (ADR-0009): the agent's own name for a tool Ordewell has no member for. */
|
|
418
|
+
toolLabel?: string;
|
|
419
|
+
toolArgs?: string;
|
|
420
|
+
toolResult?: string;
|
|
421
|
+
planToken?: string;
|
|
422
|
+
step?: ResearchStep;
|
|
423
|
+
/** The model's tool_call id, threaded on tool_call and tool_result so a
|
|
424
|
+
* surface can match the result to its pending call — robust under parallel
|
|
425
|
+
* same-tool rounds where LIFO-by-name matching mislabels summaries. */
|
|
426
|
+
toolCallId?: string;
|
|
427
|
+
/** Present when this event originates from (or reports on) one spawned research subagent (issue #34). */
|
|
428
|
+
subagentId?: string;
|
|
429
|
+
}
|
|
430
|
+
interface ThinkingBlock {
|
|
431
|
+
id: string;
|
|
432
|
+
text: string;
|
|
433
|
+
}
|
|
434
|
+
interface StreamThinkingEvent {
|
|
435
|
+
type: 'thinking';
|
|
436
|
+
block: ThinkingBlock;
|
|
437
|
+
}
|
|
438
|
+
interface StreamStepEvent {
|
|
439
|
+
type: 'step';
|
|
440
|
+
step: ResearchStep;
|
|
441
|
+
}
|
|
442
|
+
type StreamEvent = StreamThinkingEvent | StreamStepEvent;
|
|
443
|
+
interface DiscoveredModel {
|
|
444
|
+
modelId: string;
|
|
445
|
+
modelLabel: string;
|
|
446
|
+
runnerProvider?: string;
|
|
447
|
+
/**
|
|
448
|
+
* Human-facing provider name as the runner itself reports it (e.g.
|
|
449
|
+
* "OpenCode Zen" for `runnerProvider: 'opencode'`). Populated from the
|
|
450
|
+
* runner's own provider catalog when available; when absent the UI derives a
|
|
451
|
+
* label from `runnerProvider` by title-casing.
|
|
452
|
+
*/
|
|
453
|
+
runnerProviderLabel?: string;
|
|
454
|
+
/**
|
|
455
|
+
* The runner whose catalog listed this model. Stamped once, at the single
|
|
456
|
+
* `ModelDiscovery.discover` choke point, so a flat cross-runner list can
|
|
457
|
+
* still say where each entry came from — `runnerProvider` alone cannot:
|
|
458
|
+
* OpenCode reports most of its catalog as `openrouter`, which names the
|
|
459
|
+
* serving backend, not the agent Ordewell would spawn.
|
|
460
|
+
*/
|
|
461
|
+
runnerId?: string;
|
|
462
|
+
/** The runner's display name (`OpenCode`), from its manifest. */
|
|
463
|
+
runnerLabel?: string;
|
|
464
|
+
variants: {
|
|
465
|
+
id: string;
|
|
466
|
+
label: string;
|
|
467
|
+
}[];
|
|
468
|
+
}
|
|
469
|
+
type PlanStatus = 'draft' | 'approved' | 'rejected' | 'running' | 'completed';
|
|
470
|
+
/**
|
|
471
|
+
* One entry of the planner's persisted dialogue (ADR-0002). The single source
|
|
472
|
+
* of truth for both UI redisplay and conversational context. Tool-call results
|
|
473
|
+
* are NOT stored here — they live in the AI service's tool-use history;
|
|
474
|
+
* `researchLog` remains the persisted tool trace for the UI.
|
|
475
|
+
*/
|
|
476
|
+
interface ConversationMessage {
|
|
477
|
+
role: 'user' | 'assistant';
|
|
478
|
+
content: string;
|
|
479
|
+
timestamp: string;
|
|
480
|
+
/**
|
|
481
|
+
* Timeline marker: 'plan_generated' records the point in the dialogue where
|
|
482
|
+
* the plan was committed (the UI anchors the plan card there on restore);
|
|
483
|
+
* 'system' is a host-injected notice; 'compaction' is the summary a
|
|
484
|
+
* user-triggered compaction left in place of the earlier messages — always
|
|
485
|
+
* the transcript's first entry. Absent for ordinary chat turns, so
|
|
486
|
+
* sessions saved before markers existed degrade gracefully.
|
|
487
|
+
*/
|
|
488
|
+
kind?: 'plan_generated' | 'system' | 'compaction';
|
|
489
|
+
}
|
|
490
|
+
interface QueuedMessage {
|
|
491
|
+
id: string;
|
|
492
|
+
text: string;
|
|
493
|
+
timestamp: string;
|
|
494
|
+
}
|
|
495
|
+
interface PlanModificationWarnings {
|
|
496
|
+
deletedCompleted: string[];
|
|
497
|
+
changedCompleted: string[];
|
|
498
|
+
deletedInProgress: string[];
|
|
499
|
+
modifiedInProgress: string[];
|
|
500
|
+
brokenDependencies: string[];
|
|
501
|
+
}
|
|
502
|
+
declare function emptyWarnings(): PlanModificationWarnings;
|
|
503
|
+
interface LegacyPlanState {
|
|
504
|
+
tasks: Task[];
|
|
505
|
+
generatedAt: string;
|
|
506
|
+
status: PlanStatus;
|
|
507
|
+
runners: RunnerId[];
|
|
508
|
+
lastUpdated: string;
|
|
509
|
+
researchLog?: ResearchLogEntry[];
|
|
510
|
+
/** The planner dialogue — user messages and assistant messages, in order (ADR-0002). */
|
|
511
|
+
conversationHistory?: ConversationMessage[];
|
|
512
|
+
/** Full markdown PRD once written by the planner (PRD mode), also saved to .scratch/<slug>/PRD.md. */
|
|
513
|
+
prdMarkdown?: string;
|
|
514
|
+
/** Follow-ups queued while tasks execute — applied as plan modifications between batches. */
|
|
515
|
+
queuedMessages?: QueuedMessage[];
|
|
516
|
+
/**
|
|
517
|
+
* The plan's isolation run (ADR-0013), written from the orchestrator at
|
|
518
|
+
* persist time and read back only when a saved plan is adopted. It names
|
|
519
|
+
* branches and worktrees that belong to this plan alone: a fork of the plan
|
|
520
|
+
* must leave it behind rather than share it.
|
|
521
|
+
*/
|
|
522
|
+
isolation?: PlanIsolation;
|
|
523
|
+
}
|
|
524
|
+
interface Message {
|
|
525
|
+
id: string;
|
|
526
|
+
role: 'user' | 'planner' | 'system';
|
|
527
|
+
content: string;
|
|
528
|
+
timestamp: number;
|
|
529
|
+
}
|
|
530
|
+
interface TaskSnapshot extends Task {
|
|
531
|
+
completedAt: number;
|
|
532
|
+
verdict?: Verdict;
|
|
533
|
+
retryCount: number;
|
|
534
|
+
finalized: boolean;
|
|
535
|
+
}
|
|
536
|
+
type PlanState = {
|
|
537
|
+
phase: 'planning';
|
|
538
|
+
history: Message[];
|
|
539
|
+
message: string;
|
|
540
|
+
pendingTasks: Task[];
|
|
541
|
+
} | {
|
|
542
|
+
phase: 'executing';
|
|
543
|
+
history: Message[];
|
|
544
|
+
message: string;
|
|
545
|
+
executionLog: TaskSnapshot[];
|
|
546
|
+
pendingTasks: Task[];
|
|
547
|
+
goal: string;
|
|
548
|
+
runners: string[];
|
|
549
|
+
status: PlanStatus;
|
|
550
|
+
};
|
|
551
|
+
declare function migratePlanState(raw: unknown): PlanState;
|
|
552
|
+
declare function migrateLegacyPlan(legacy: LegacyPlanState): PlanState;
|
|
553
|
+
|
|
554
|
+
declare function createTask(overrides?: Partial<Task>): Task;
|
|
555
|
+
declare function createEmptyPlan(): LegacyPlanState;
|
|
556
|
+
declare function flattenTasks(tasks: Task[]): Task[];
|
|
557
|
+
/** A flattened task with the parent it hangs under, null for a top-level task. */
|
|
558
|
+
interface TaskWithParent {
|
|
559
|
+
task: Task;
|
|
560
|
+
parent: Task | null;
|
|
561
|
+
}
|
|
562
|
+
declare function flattenTasksWithParents(tasks: Task[]): TaskWithParent[];
|
|
563
|
+
declare function migrateTask(task: Record<string, unknown>): Task;
|
|
564
|
+
declare function addTaskToPlan(tasks: Task[], partial: Partial<Task>): Task[];
|
|
565
|
+
declare function removeTaskFromPlan(tasks: Task[], taskId: string): Task[];
|
|
566
|
+
declare function updateTaskInPlan(tasks: Task[], taskId: string, changes: Partial<Task>): Task[];
|
|
567
|
+
declare function renumberTasks(tasks: Task[]): Task[];
|
|
568
|
+
declare function validateModifiedPlan(original: Task[], modified: Task[]): PlanModificationWarnings;
|
|
569
|
+
interface ActiveTaskSession {
|
|
570
|
+
id: string;
|
|
571
|
+
taskId: string;
|
|
572
|
+
}
|
|
573
|
+
interface ValidationResult {
|
|
574
|
+
valid: boolean;
|
|
575
|
+
errors: string[];
|
|
576
|
+
}
|
|
577
|
+
interface ValidationContext {
|
|
578
|
+
executionLog: TaskSnapshot[];
|
|
579
|
+
oldPending: Task[];
|
|
580
|
+
newPending: Task[];
|
|
581
|
+
activeSessions: Map<string, ActiveTaskSession>;
|
|
582
|
+
}
|
|
583
|
+
type ValidationCheck = (ctx: ValidationContext) => ValidationResult;
|
|
584
|
+
declare function warningsText(w: PlanModificationWarnings): string | null;
|
|
585
|
+
|
|
586
|
+
export { type ValidationContext as $, type ActiveTaskSession as A, type RunnerId as B, type ConversationMessage as C, type DiscoveredMode as D, type StreamStepEvent as E, type StreamThinkingEvent as F, type TaskIsolation as G, type TaskIsolationState as H, type IWorktreeIsolation as I, type TaskMode as J, type TaskModelAssignment as K, type LegacyPlanState as L, type Message as M, type TaskOutputSummary as N, type TaskSnapshot as O, type PlanIsolation as P, type QueuedMessage as Q, type RepoGroupLayout as R, type StreamEvent as S, type Task as T, type TaskStatus as U, type TaskType as V, type TaskWithParent as W, type ThinkingBlock as X, type UserPromptEntry as Y, type UserStep as Z, type ValidationCheck as _, type DiscoveredModel as a, type ValidationResult as a0, type Verdict as a1, type VerificationCheck as a2, addTaskToPlan as a3, createEmptyPlan as a4, createTask as a5, emptyWarnings as a6, flattenTasks as a7, flattenTasksWithParents as a8, migrateLegacyPlan as a9, migratePlanState as aa, migrateTask as ab, removeTaskFromPlan as ac, renumberTasks as ad, updateTaskInPlan as ae, validateModifiedPlan as af, warningsText as ag, type IsolationAvailability as b, type IsolationHandoff as c, type IsolationHandoffRepo as d, type IsolationInactiveReason as e, type IsolationLandedTask as f, type IsolationLanding as g, type IsolationMergeBlock as h, type IsolationMergeBlockReason as i, type IsolationMergeResult as j, type IsolationOutcome as k, type IsolationRepo as l, type IsolationRun as m, type IsolationTaskRecord as n, type IsolationTaskRepo as o, type IsolationTaskStatus as p, type IsolationView as q, type PlanModificationWarnings as r, type PlanState as s, type PlanStatus as t, type PreparedTask as u, type ResearchLogEntry as v, type ResearchProgress as w, type ResearchStep as x, type ResearchStepOutcome as y, type ResearchToolType as z };
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
renumberTasks,
|
|
11
11
|
stripTrailingCommas,
|
|
12
12
|
updateTaskInPlan
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-JVMDEHRQ.mjs";
|
|
14
14
|
|
|
15
15
|
// src/services/ModelAllowlistResolver.ts
|
|
16
16
|
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
@@ -128,10 +128,10 @@ function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
|
128
128
|
const models = catalog.modelsByRunner[runner];
|
|
129
129
|
if (models) {
|
|
130
130
|
const modelId = assignedModel.modelId;
|
|
131
|
-
|
|
131
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
132
|
+
if (!models.some((m) => m.modelId === modelId) && !allowed?.includes(modelId)) {
|
|
132
133
|
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
133
134
|
}
|
|
134
|
-
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
135
135
|
if (allowed && !allowed.includes(modelId)) {
|
|
136
136
|
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
137
137
|
}
|
|
@@ -192,6 +192,29 @@ var UPDATABLE_FIELDS = [
|
|
|
192
192
|
"autonomy",
|
|
193
193
|
"sliceType"
|
|
194
194
|
];
|
|
195
|
+
function taskOpsProtocol(executionNote = "") {
|
|
196
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
197
|
+
return [
|
|
198
|
+
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
199
|
+
' {"taskOps": [',
|
|
200
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
201
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
202
|
+
' {"op":"remove","taskId":"<id or #order>"},',
|
|
203
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
204
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
205
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
206
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
207
|
+
" ]}",
|
|
208
|
+
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
209
|
+
'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
|
|
210
|
+
'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
|
|
211
|
+
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
212
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
213
|
+
'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + executionNote,
|
|
214
|
+
'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
|
|
215
|
+
'"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
|
|
216
|
+
];
|
|
217
|
+
}
|
|
195
218
|
function textHasTaskOps(text) {
|
|
196
219
|
return text.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
197
220
|
}
|
|
@@ -862,12 +885,13 @@ function serializeTask(t) {
|
|
|
862
885
|
userStoriesCovered: t.userStoriesCovered || void 0
|
|
863
886
|
};
|
|
864
887
|
}
|
|
865
|
-
function serializeTaskStatus(t, idleSince = null) {
|
|
888
|
+
function serializeTaskStatus(t, idleSince = null, isolation = null) {
|
|
866
889
|
return {
|
|
867
890
|
id: t.id,
|
|
868
891
|
status: t.status,
|
|
869
892
|
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
870
|
-
idleSince
|
|
893
|
+
idleSince,
|
|
894
|
+
...isolation ? { isolation } : {}
|
|
871
895
|
};
|
|
872
896
|
}
|
|
873
897
|
function serializePlan(plan) {
|
|
@@ -900,7 +924,7 @@ export {
|
|
|
900
924
|
clampThinkingEffort,
|
|
901
925
|
coerceAssignments,
|
|
902
926
|
validateTaskEdit,
|
|
903
|
-
|
|
927
|
+
taskOpsProtocol,
|
|
904
928
|
textHasTaskOps,
|
|
905
929
|
parseTaskOpsJson,
|
|
906
930
|
canMergeTasks,
|
|
@@ -918,4 +942,4 @@ export {
|
|
|
918
942
|
CHECKPOINT_TRUNCATE_LENGTH,
|
|
919
943
|
truncateCheckpointSummary
|
|
920
944
|
};
|
|
921
|
-
//# sourceMappingURL=chunk-
|
|
945
|
+
//# sourceMappingURL=chunk-GWPIYDQW.mjs.map
|