@ordewell/core 0.4.9 → 0.4.11
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-CbE6gKLI.d.ts → ITerminalRunner-BTaKZ659.d.mts} +2 -2
- package/dist/{ITerminalRunner-1NAv8wCC.d.mts → ITerminalRunner-DVwORcV9.d.ts} +2 -2
- package/dist/ModeResolver-CCKYqdZC.d.mts +44 -0
- package/dist/ModeResolver-CzZd-i6T.d.ts +44 -0
- package/dist/{Task-1NwmUImI.d.mts → Task-cGrfYRXK.d.mts} +8 -1
- package/dist/{Task-1NwmUImI.d.ts → Task-cGrfYRXK.d.ts} +8 -1
- package/dist/chunk-DNNZGH3I.mjs +862 -0
- package/dist/chunk-DNNZGH3I.mjs.map +1 -0
- package/dist/chunk-HPNUFBIT.mjs +19 -0
- package/dist/chunk-HPNUFBIT.mjs.map +1 -0
- package/dist/{chunk-SRGQBGI5.mjs → chunk-N6HTBUKG.mjs} +13 -1
- package/dist/{chunk-SRGQBGI5.mjs.map → chunk-N6HTBUKG.mjs.map} +1 -1
- package/dist/{chunk-YXAIQKVE.mjs → chunk-UEJ3SZQ7.mjs} +2 -2
- package/dist/index.d.mts +284 -80
- package/dist/index.d.ts +284 -80
- package/dist/index.js +1670 -820
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1067 -597
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +17 -0
- package/dist/order-labels.d.ts +17 -0
- package/dist/order-labels.js +44 -0
- package/dist/order-labels.js.map +1 -0
- package/dist/order-labels.mjs +9 -0
- package/dist/order-labels.mjs.map +1 -0
- package/dist/{parsing-RjN9JbyG.d.ts → parsing-BT-dvFcu.d.mts} +5 -43
- package/dist/{parsing-xpozY5ac.d.mts → parsing-D87zKtTe.d.ts} +5 -43
- package/dist/parsing.d.mts +3 -2
- package/dist/parsing.d.ts +3 -2
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -2
- package/dist/{plan-utils-nfHKSbd3.d.mts → plan-utils-BHLbBym3.d.mts} +27 -5
- package/dist/{plan-utils-Cgc0cCH1.d.ts → plan-utils-akoB6YCL.d.ts} +27 -5
- package/dist/plan-utils.d.mts +3 -2
- package/dist/plan-utils.d.ts +3 -2
- package/dist/plan-utils.js +1 -9
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +3 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.d.ts +2 -2
- package/package.json +12 -2
- package/skills/grilling/SKILL.md +31 -0
- package/skills/to-spec/SKILL.md +64 -0
- package/dist/chunk-4KIWPW4K.mjs +0 -510
- package/dist/chunk-4KIWPW4K.mjs.map +0 -1
- /package/dist/{chunk-YXAIQKVE.mjs.map → chunk-UEJ3SZQ7.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
AbstractRunner: () => AbstractRunner,
|
|
37
37
|
AbstractTerminalSession: () => AbstractTerminalSession,
|
|
38
38
|
ApprovalPolicy: () => ApprovalPolicy,
|
|
39
|
+
BUILTIN_SKILL_NAMES: () => BUILTIN_SKILL_NAMES,
|
|
39
40
|
BaseAiService: () => BaseAiService,
|
|
40
41
|
BaseConfig: () => BaseConfig,
|
|
41
42
|
BaseFileSystem: () => BaseFileSystem,
|
|
@@ -94,8 +95,14 @@ __export(index_exports, {
|
|
|
94
95
|
SYMBOL_LANGUAGES: () => SYMBOL_LANGUAGES,
|
|
95
96
|
Session: () => Session,
|
|
96
97
|
SettingsService: () => SettingsService,
|
|
98
|
+
SkillsService: () => SkillsService,
|
|
97
99
|
StdioAgentAdapter: () => StdioAgentAdapter,
|
|
98
100
|
TASK_OPS_ENVELOPE_KEY: () => TASK_OPS_ENVELOPE_KEY,
|
|
101
|
+
TASK_QUERY_ANSWER_OR_OPS: () => TASK_QUERY_ANSWER_OR_OPS,
|
|
102
|
+
TASK_QUERY_ENVELOPE_KEY: () => TASK_QUERY_ENVELOPE_KEY,
|
|
103
|
+
TASK_QUERY_FIELDS: () => TASK_QUERY_FIELDS,
|
|
104
|
+
TASK_QUERY_PROTOCOL: () => TASK_QUERY_PROTOCOL,
|
|
105
|
+
TASK_QUERY_REMINDER: () => TASK_QUERY_REMINDER,
|
|
99
106
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION: () => TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
100
107
|
TaskOrchestrator: () => TaskOrchestrator,
|
|
101
108
|
TmuxRunner: () => TmuxRunner,
|
|
@@ -154,6 +161,7 @@ __export(index_exports, {
|
|
|
154
161
|
configuredProviders: () => configuredProviders,
|
|
155
162
|
createAiService: () => createAiService,
|
|
156
163
|
createEmptyPlan: () => createEmptyPlan,
|
|
164
|
+
createSkillsService: () => createSkillsService,
|
|
157
165
|
createTask: () => createTask,
|
|
158
166
|
daemonTokenPath: () => daemonTokenPath,
|
|
159
167
|
defaultLogger: () => defaultLogger,
|
|
@@ -180,12 +188,14 @@ __export(index_exports, {
|
|
|
180
188
|
filterModelsForPrompt: () => filterModelsForPrompt,
|
|
181
189
|
filteredBuildModes: () => filteredBuildModes,
|
|
182
190
|
flattenTasks: () => flattenTasks,
|
|
191
|
+
flattenTasksWithParents: () => flattenTasksWithParents,
|
|
183
192
|
formatSearchOutput: () => formatSearchOutput,
|
|
184
193
|
generatePlanWithRepair: () => generatePlanWithRepair,
|
|
185
194
|
getLatestSession: () => getLatestSession,
|
|
186
195
|
getProviderMeta: () => getProviderMeta,
|
|
187
196
|
getSettingsPath: () => getSettingsPath,
|
|
188
197
|
getStateDir: () => getStateDir,
|
|
198
|
+
globalDataDir: () => globalDataDir,
|
|
189
199
|
grantScopeFor: () => grantScopeFor,
|
|
190
200
|
hasTmux: () => hasTmux,
|
|
191
201
|
includeGlobFor: () => includeGlobFor,
|
|
@@ -205,6 +215,7 @@ __export(index_exports, {
|
|
|
205
215
|
looksLikePlanAttempt: () => looksLikePlanAttempt,
|
|
206
216
|
mapAgentTool: () => mapAgentTool,
|
|
207
217
|
migrateLegacyPlan: () => migrateLegacyPlan,
|
|
218
|
+
migrateOldConfigDir: () => migrateOldConfigDir,
|
|
208
219
|
migratePlanState: () => migratePlanState,
|
|
209
220
|
migrateTask: () => migrateTask,
|
|
210
221
|
mintDaemonToken: () => mintDaemonToken,
|
|
@@ -216,6 +227,7 @@ __export(index_exports, {
|
|
|
216
227
|
parsePartialPlan: () => parsePartialPlan,
|
|
217
228
|
parsePlanJson: () => parsePlanJson,
|
|
218
229
|
parseTaskOpsJson: () => parseTaskOpsJson,
|
|
230
|
+
parseTaskQueryJson: () => parseTaskQueryJson,
|
|
219
231
|
pendingEditRulesBlock: () => pendingEditRulesBlock,
|
|
220
232
|
planDirectLaunch: () => planDirectLaunch,
|
|
221
233
|
planShellLaunch: () => planShellLaunch,
|
|
@@ -224,11 +236,13 @@ __export(index_exports, {
|
|
|
224
236
|
providerForRunner: () => providerForRunner,
|
|
225
237
|
reEmitPlanPrompt: () => reEmitPlanPrompt,
|
|
226
238
|
reEmitTaskOpsPrompt: () => reEmitTaskOpsPrompt,
|
|
239
|
+
reEmitTaskQueryPrompt: () => reEmitTaskQueryPrompt,
|
|
227
240
|
readDaemonToken: () => readDaemonToken,
|
|
228
241
|
referencePattern: () => referencePattern,
|
|
229
242
|
removeTaskFromPlan: () => removeTaskFromPlan,
|
|
230
243
|
renderPlanMap: () => renderPlanMap,
|
|
231
244
|
renderPriorOutputs: () => renderPriorOutputs,
|
|
245
|
+
renderTaskQueryAnswer: () => renderTaskQueryAnswer,
|
|
232
246
|
renumberTasks: () => renumberTasks,
|
|
233
247
|
repairLoop: () => repairLoop,
|
|
234
248
|
researchShellWarning: () => researchShellWarning,
|
|
@@ -236,10 +250,12 @@ __export(index_exports, {
|
|
|
236
250
|
resolveArgs: () => resolveArgs,
|
|
237
251
|
resolveDefaultMode: () => resolveDefaultMode,
|
|
238
252
|
resolveModelShortcut: () => resolveModelShortcut,
|
|
253
|
+
resolveOrderLabel: () => resolveOrderLabel,
|
|
239
254
|
resolvePluginInstallDir: () => resolvePluginInstallDir,
|
|
240
255
|
resolveProvider: () => resolveProvider,
|
|
241
256
|
resolveProviderFromPrefix: () => resolveProviderFromPrefix,
|
|
242
257
|
resolveResearchShell: () => resolveResearchShell,
|
|
258
|
+
resolveSkillInvocation: () => resolveSkillInvocation,
|
|
243
259
|
resolveTaskMode: () => resolveTaskMode,
|
|
244
260
|
resolveWithin: () => resolveWithin,
|
|
245
261
|
retargetTaskRunner: () => retargetTaskRunner,
|
|
@@ -262,7 +278,10 @@ __export(index_exports, {
|
|
|
262
278
|
summarizeOutput: () => summarizeOutput,
|
|
263
279
|
summarizeToolCall: () => summarizeToolCall,
|
|
264
280
|
taskOpsRejectedPrompt: () => taskOpsRejectedPrompt,
|
|
281
|
+
taskOrderLabel: () => taskOrderLabel,
|
|
282
|
+
taskQuerySignature: () => taskQuerySignature,
|
|
265
283
|
textHasTaskOps: () => textHasTaskOps,
|
|
284
|
+
textHasTaskQuery: () => textHasTaskQuery,
|
|
266
285
|
tmuxSessionName: () => tmuxSessionName,
|
|
267
286
|
tmuxSocketName: () => tmuxSocketName,
|
|
268
287
|
tmuxWindowName: () => tmuxWindowName,
|
|
@@ -283,6 +302,22 @@ module.exports = __toCommonJS(index_exports);
|
|
|
283
302
|
|
|
284
303
|
// src/models/Task.ts
|
|
285
304
|
var import_uuid = require("uuid");
|
|
305
|
+
|
|
306
|
+
// src/order-labels.ts
|
|
307
|
+
function taskOrderLabel(task, parent) {
|
|
308
|
+
return parent ? `${parent.order}.${task.order}` : String(task.order);
|
|
309
|
+
}
|
|
310
|
+
function resolveOrderLabel(tasks, label) {
|
|
311
|
+
const parts = label.split(".");
|
|
312
|
+
if (parts.length > 2 || parts.some((p) => !/^\d+$/.test(p))) return void 0;
|
|
313
|
+
const [topOrder, childOrder] = parts.map(Number);
|
|
314
|
+
const top = tasks.find((t) => t.order === topOrder);
|
|
315
|
+
if (!top) return void 0;
|
|
316
|
+
if (childOrder === void 0) return top;
|
|
317
|
+
return top.subtasks.find((s) => s.order === childOrder);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/models/Task.ts
|
|
286
321
|
function emptyWarnings() {
|
|
287
322
|
return { deletedCompleted: [], changedCompleted: [], deletedInProgress: [], modifiedInProgress: [], brokenDependencies: [] };
|
|
288
323
|
}
|
|
@@ -467,6 +502,15 @@ function createEmptyPlan() {
|
|
|
467
502
|
function flattenTasks(tasks) {
|
|
468
503
|
return tasks.flatMap((t) => [t, ...flattenTasks(t.subtasks ?? [])]);
|
|
469
504
|
}
|
|
505
|
+
function flattenTasksWithParents(tasks) {
|
|
506
|
+
const rows = [];
|
|
507
|
+
const walk = (t, parent) => {
|
|
508
|
+
rows.push({ task: t, parent });
|
|
509
|
+
for (const sub of t.subtasks ?? []) walk(sub, t);
|
|
510
|
+
};
|
|
511
|
+
for (const t of tasks) walk(t, null);
|
|
512
|
+
return rows;
|
|
513
|
+
}
|
|
470
514
|
function migrateTask(task) {
|
|
471
515
|
if (!task.assignedModel) {
|
|
472
516
|
task.assignedModel = void 0;
|
|
@@ -3093,7 +3137,7 @@ function redactSecrets(text) {
|
|
|
3093
3137
|
const { value, tail } = trimTrailingPunctuation(raw);
|
|
3094
3138
|
if (value.length < 8) return full;
|
|
3095
3139
|
if (looksLikeCode(value) || value.startsWith("$")) return full;
|
|
3096
|
-
if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && /\d/.test(value)) {
|
|
3140
|
+
if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && (/\d/.test(value) || value.length >= 12)) {
|
|
3097
3141
|
return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
|
|
3098
3142
|
}
|
|
3099
3143
|
if (IDENTIFIER.test(value) || SLUG.test(value)) return full;
|
|
@@ -3132,33 +3176,33 @@ function grepOptions(args) {
|
|
|
3132
3176
|
};
|
|
3133
3177
|
}
|
|
3134
3178
|
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
3135
|
-
async function executeTool(name, args,
|
|
3136
|
-
const outcome = await runTool(name, args,
|
|
3179
|
+
async function executeTool(name, args, fs12, fetcher, signal) {
|
|
3180
|
+
const outcome = await runTool(name, args, fs12, fetcher, signal);
|
|
3137
3181
|
return { ...outcome, output: redactSecrets(outcome.output) };
|
|
3138
3182
|
}
|
|
3139
|
-
async function runTool(name, args,
|
|
3183
|
+
async function runTool(name, args, fs12, fetcher, signal) {
|
|
3140
3184
|
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
3141
3185
|
switch (name) {
|
|
3142
3186
|
case "read_file": {
|
|
3143
3187
|
const offset = num(args.offset);
|
|
3144
3188
|
const limit = num(args.limit);
|
|
3145
3189
|
const maxBytes = num(args.maxBytes);
|
|
3146
|
-
return
|
|
3190
|
+
return fs12.readFile(String(args.path ?? ""), { offset, limit, maxBytes });
|
|
3147
3191
|
}
|
|
3148
3192
|
case "read_files": {
|
|
3149
3193
|
const paths = Array.isArray(args.paths) ? args.paths.map(String) : [];
|
|
3150
|
-
return
|
|
3194
|
+
return fs12.readFiles(paths);
|
|
3151
3195
|
}
|
|
3152
3196
|
case "glob":
|
|
3153
|
-
return
|
|
3197
|
+
return fs12.glob(String(args.pattern ?? ""), { path: str(args.path), headLimit: num(args.head_limit ?? args.headLimit) });
|
|
3154
3198
|
case "grep":
|
|
3155
|
-
return
|
|
3199
|
+
return fs12.grep(String(args.pattern ?? ""), grepOptions(args));
|
|
3156
3200
|
case "find_symbol":
|
|
3157
|
-
return
|
|
3201
|
+
return fs12.findSymbol(String(args.symbol ?? ""), { language: str(args.language), path: str(args.path) });
|
|
3158
3202
|
case "list_dir":
|
|
3159
|
-
return
|
|
3203
|
+
return fs12.listDir(String(args.path ?? ""), num(args.depth));
|
|
3160
3204
|
case "bash":
|
|
3161
|
-
return
|
|
3205
|
+
return fs12.bash(String(args.command ?? ""), signal);
|
|
3162
3206
|
case "fetch": {
|
|
3163
3207
|
if (!fetcher) return { success: false, output: "Web fetching is not available in this environment.", truncated: false };
|
|
3164
3208
|
const url = String(args.url ?? "");
|
|
@@ -4237,7 +4281,7 @@ function formatError(err) {
|
|
|
4237
4281
|
var defaultLogger = new ConsoleLogger();
|
|
4238
4282
|
|
|
4239
4283
|
// src/services/BaseAiService.ts
|
|
4240
|
-
var
|
|
4284
|
+
var import_uuid3 = require("uuid");
|
|
4241
4285
|
|
|
4242
4286
|
// src/services/ModeResolver.ts
|
|
4243
4287
|
function runnerModesFrom(registry, runners) {
|
|
@@ -4310,14 +4354,7 @@ function resolveTaskMode(emittedMode, runnerId, runnerModes, autonomousDefault =
|
|
|
4310
4354
|
|
|
4311
4355
|
// src/services/plannerModes.ts
|
|
4312
4356
|
var MODE_TOGGLES = [
|
|
4313
|
-
// Both of these interview the user, and a one-shot run has nobody to ask —
|
|
4314
|
-
// its prompt says so outright. Their absence there is a decision, not a gap.
|
|
4315
|
-
{ id: "grill-me", settingsKey: "grillMe", runtimeKey: "grillMeEnabled", scopes: ["chat"] },
|
|
4316
|
-
{ id: "prd", settingsKey: "prd", runtimeKey: "prdEnabled", scopes: ["chat"] },
|
|
4317
|
-
// Structural: append a final review task. Nothing about it needs a dialogue.
|
|
4318
|
-
{ id: "review", settingsKey: "review", runtimeKey: "reviewEnabled", scopes: ["chat", "one-shot"] },
|
|
4319
4357
|
{ id: "verify", settingsKey: "verification", runtimeKey: "verificationEnabled", scopes: ["chat", "one-shot"] },
|
|
4320
|
-
{ id: "research-subagents", settingsKey: "researchSubagents", runtimeKey: "researchSubagentsEnabled", scopes: ["chat", "one-shot"] },
|
|
4321
4358
|
{ id: "tdd", settingsKey: "tdd", runtimeKey: "tddEnabled", scopes: ["task"] }
|
|
4322
4359
|
];
|
|
4323
4360
|
function plannerRuntimeToggles(settings) {
|
|
@@ -4327,28 +4364,16 @@ function plannerRuntimeToggles(settings) {
|
|
|
4327
4364
|
}
|
|
4328
4365
|
var DEFAULT_PLANNER_MODES = {
|
|
4329
4366
|
autonomousDefault: true,
|
|
4330
|
-
|
|
4331
|
-
prd: false,
|
|
4332
|
-
review: false,
|
|
4333
|
-
verification: false,
|
|
4334
|
-
researchSubagents: false
|
|
4367
|
+
verification: false
|
|
4335
4368
|
};
|
|
4336
4369
|
function plannerModesFrom(settings, autonomousDefault) {
|
|
4337
4370
|
return {
|
|
4338
4371
|
autonomousDefault,
|
|
4339
|
-
|
|
4340
|
-
prd: settings.prdEnabled ?? false,
|
|
4341
|
-
review: settings.reviewEnabled ?? false,
|
|
4342
|
-
verification: settings.verificationEnabled ?? false,
|
|
4343
|
-
researchSubagents: settings.researchSubagentsEnabled ?? false
|
|
4372
|
+
verification: settings.verificationEnabled ?? false
|
|
4344
4373
|
};
|
|
4345
4374
|
}
|
|
4346
4375
|
var MODE_FIELD = {
|
|
4347
|
-
"grill-me": "grillMe",
|
|
4348
|
-
prd: "prd",
|
|
4349
|
-
review: "review",
|
|
4350
4376
|
verify: "verification",
|
|
4351
|
-
"research-subagents": "researchSubagents",
|
|
4352
4377
|
tdd: null
|
|
4353
4378
|
};
|
|
4354
4379
|
function modesFor(scope, modes) {
|
|
@@ -4360,8 +4385,323 @@ function modesFor(scope, modes) {
|
|
|
4360
4385
|
return scoped;
|
|
4361
4386
|
}
|
|
4362
4387
|
|
|
4388
|
+
// src/services/JsonExtractor.ts
|
|
4389
|
+
var PLAN_ENVELOPE_KEY = "tasks";
|
|
4390
|
+
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
4391
|
+
var TASK_QUERY_ENVELOPE_KEY = "taskQuery";
|
|
4392
|
+
var PlanParseError = class extends Error {
|
|
4393
|
+
raw;
|
|
4394
|
+
/**
|
|
4395
|
+
* True when the reply was cut off mid-object (output-token limit) rather
|
|
4396
|
+
* than malformed — the two need different repairs: a truncated emission will
|
|
4397
|
+
* be cut at the same point again unless the retry frees context or asks for
|
|
4398
|
+
* terser output.
|
|
4399
|
+
*/
|
|
4400
|
+
truncated;
|
|
4401
|
+
/**
|
|
4402
|
+
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
4403
|
+
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
4404
|
+
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
4405
|
+
* the rule it broke.
|
|
4406
|
+
*/
|
|
4407
|
+
semantic;
|
|
4408
|
+
constructor(message, raw, opts) {
|
|
4409
|
+
super(message);
|
|
4410
|
+
this.name = "PlanParseError";
|
|
4411
|
+
this.raw = raw;
|
|
4412
|
+
this.truncated = opts?.truncated ?? false;
|
|
4413
|
+
this.semantic = opts?.semantic ?? false;
|
|
4414
|
+
}
|
|
4415
|
+
};
|
|
4416
|
+
function stripModelNoise(raw) {
|
|
4417
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
4418
|
+
}
|
|
4419
|
+
function balancedObjectAt(text, start) {
|
|
4420
|
+
let depth = 0;
|
|
4421
|
+
let inString = false;
|
|
4422
|
+
let escaped = false;
|
|
4423
|
+
for (let i = start; i < text.length; i++) {
|
|
4424
|
+
const ch = text[i];
|
|
4425
|
+
if (inString) {
|
|
4426
|
+
if (escaped) escaped = false;
|
|
4427
|
+
else if (ch === "\\") escaped = true;
|
|
4428
|
+
else if (ch === '"') inString = false;
|
|
4429
|
+
continue;
|
|
4430
|
+
}
|
|
4431
|
+
if (ch === '"') inString = true;
|
|
4432
|
+
else if (ch === "{") depth++;
|
|
4433
|
+
else if (ch === "}") {
|
|
4434
|
+
depth--;
|
|
4435
|
+
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
return { json: text.slice(start), balanced: false };
|
|
4439
|
+
}
|
|
4440
|
+
function extractObjectsWithKey(raw, key) {
|
|
4441
|
+
const stripped = stripModelNoise(raw);
|
|
4442
|
+
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
4443
|
+
const matches = [];
|
|
4444
|
+
let firstObject = null;
|
|
4445
|
+
let sawBrace = false;
|
|
4446
|
+
let sawUnbalanced = false;
|
|
4447
|
+
let inString = false;
|
|
4448
|
+
let escaped = false;
|
|
4449
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
4450
|
+
const ch = stripped[i];
|
|
4451
|
+
if (inString) {
|
|
4452
|
+
if (escaped) escaped = false;
|
|
4453
|
+
else if (ch === "\\") escaped = true;
|
|
4454
|
+
else if (ch === '"') inString = false;
|
|
4455
|
+
continue;
|
|
4456
|
+
}
|
|
4457
|
+
if (ch === '"') {
|
|
4458
|
+
inString = true;
|
|
4459
|
+
continue;
|
|
4460
|
+
}
|
|
4461
|
+
if (ch !== "{") continue;
|
|
4462
|
+
sawBrace = true;
|
|
4463
|
+
const obj = balancedObjectAt(stripped, i);
|
|
4464
|
+
if (obj.balanced && keyRe.test(obj.json)) {
|
|
4465
|
+
matches.push(obj.json);
|
|
4466
|
+
i += obj.json.length - 1;
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
if (!obj.balanced) sawUnbalanced = true;
|
|
4470
|
+
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
4471
|
+
}
|
|
4472
|
+
return {
|
|
4473
|
+
matches,
|
|
4474
|
+
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
4475
|
+
sawBrace,
|
|
4476
|
+
sawUnbalanced
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
function extractObjectWithBalance(raw) {
|
|
4480
|
+
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
4481
|
+
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
4482
|
+
return { ...fallback, sawBrace };
|
|
4483
|
+
}
|
|
4484
|
+
function extractJsonObject(raw) {
|
|
4485
|
+
return extractObjectWithBalance(raw).json;
|
|
4486
|
+
}
|
|
4487
|
+
function stripTrailingCommas(json) {
|
|
4488
|
+
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
4489
|
+
}
|
|
4490
|
+
function escapeControlCharsInStrings(json) {
|
|
4491
|
+
let out = "";
|
|
4492
|
+
let inString = false;
|
|
4493
|
+
let escaped = false;
|
|
4494
|
+
for (const ch of json) {
|
|
4495
|
+
if (inString) {
|
|
4496
|
+
if (escaped) {
|
|
4497
|
+
out += ch;
|
|
4498
|
+
escaped = false;
|
|
4499
|
+
continue;
|
|
4500
|
+
}
|
|
4501
|
+
if (ch === "\\") {
|
|
4502
|
+
out += ch;
|
|
4503
|
+
escaped = true;
|
|
4504
|
+
continue;
|
|
4505
|
+
}
|
|
4506
|
+
if (ch === '"') {
|
|
4507
|
+
out += ch;
|
|
4508
|
+
inString = false;
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
if (ch === "\n") {
|
|
4512
|
+
out += "\\n";
|
|
4513
|
+
continue;
|
|
4514
|
+
}
|
|
4515
|
+
if (ch === "\r") {
|
|
4516
|
+
out += "\\r";
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4519
|
+
if (ch === " ") {
|
|
4520
|
+
out += "\\t";
|
|
4521
|
+
continue;
|
|
4522
|
+
}
|
|
4523
|
+
out += ch;
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
if (ch === '"') inString = true;
|
|
4527
|
+
out += ch;
|
|
4528
|
+
}
|
|
4529
|
+
return out;
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
// src/services/TaskQuery.ts
|
|
4533
|
+
var TASK_QUERY_FIELDS = [
|
|
4534
|
+
"description",
|
|
4535
|
+
"prompt",
|
|
4536
|
+
"userSteps",
|
|
4537
|
+
"verdict",
|
|
4538
|
+
"outputSummary",
|
|
4539
|
+
"userStoriesCovered"
|
|
4540
|
+
];
|
|
4541
|
+
function textHasTaskQuery(text) {
|
|
4542
|
+
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
4543
|
+
}
|
|
4544
|
+
function parseTaskQueryJson(text) {
|
|
4545
|
+
const { matches, fallback } = extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY);
|
|
4546
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
4547
|
+
let lastError = null;
|
|
4548
|
+
for (const json of candidates) {
|
|
4549
|
+
try {
|
|
4550
|
+
return parseTaskQueryObject(json, text);
|
|
4551
|
+
} catch (err) {
|
|
4552
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
4553
|
+
if (!lastError) lastError = err;
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-query reply", text);
|
|
4557
|
+
}
|
|
4558
|
+
function parseTaskQueryObject(json, text) {
|
|
4559
|
+
let parsed;
|
|
4560
|
+
try {
|
|
4561
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
4562
|
+
} catch (err) {
|
|
4563
|
+
throw new PlanParseError(`Task-query JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
|
|
4564
|
+
}
|
|
4565
|
+
const raw = parsed.taskQuery;
|
|
4566
|
+
const body = Array.isArray(raw) ? { tasks: raw } : raw;
|
|
4567
|
+
if (typeof body !== "object" || body === null) {
|
|
4568
|
+
throw new PlanParseError('"taskQuery" must be an object (or an array of task references)', text);
|
|
4569
|
+
}
|
|
4570
|
+
const { tasks: rawTasks, fields: rawFields, catalog: rawCatalog } = body;
|
|
4571
|
+
const tasks = [];
|
|
4572
|
+
if (rawTasks !== void 0) {
|
|
4573
|
+
if (!Array.isArray(rawTasks)) throw new PlanParseError('"tasks" in a taskQuery must be an array of task references', text);
|
|
4574
|
+
for (const ref of rawTasks) {
|
|
4575
|
+
if (typeof ref !== "string" && typeof ref !== "number") {
|
|
4576
|
+
throw new PlanParseError(`Invalid task reference "${String(ref)}" in taskQuery`, text);
|
|
4577
|
+
}
|
|
4578
|
+
tasks.push(String(ref).trim());
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
let fields;
|
|
4582
|
+
if (rawFields !== void 0) {
|
|
4583
|
+
if (!Array.isArray(rawFields)) throw new PlanParseError('"fields" in a taskQuery must be an array of field names', text);
|
|
4584
|
+
fields = [];
|
|
4585
|
+
for (const f of rawFields) {
|
|
4586
|
+
if (typeof f !== "string" || !TASK_QUERY_FIELDS.includes(f)) {
|
|
4587
|
+
throw new PlanParseError(`Unknown taskQuery field "${String(f)}" \u2014 choose from ${TASK_QUERY_FIELDS.join(", ")}`, text);
|
|
4588
|
+
}
|
|
4589
|
+
fields.push(f);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
const catalog = rawCatalog === true;
|
|
4593
|
+
if (tasks.length === 0 && !catalog) {
|
|
4594
|
+
throw new PlanParseError('A taskQuery must name at least one task or set "catalog": true', text);
|
|
4595
|
+
}
|
|
4596
|
+
return { tasks, fields, catalog };
|
|
4597
|
+
}
|
|
4598
|
+
function taskQuerySignature(query) {
|
|
4599
|
+
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog]);
|
|
4600
|
+
}
|
|
4601
|
+
var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
|
|
4602
|
+
var TASK_QUERY_PROTOCOL = [
|
|
4603
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
4604
|
+
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
|
|
4605
|
+
` {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>", "..."],"fields":[${TASK_QUERY_FIELDS.map((f) => `"${f}"`).join(", ")}],"catalog":true}}`,
|
|
4606
|
+
'- "tasks": one or more task references. Ask for everything you need in ONE query \u2014 each query costs a round-trip.',
|
|
4607
|
+
'- "fields": optional. Omit it to get every field above.',
|
|
4608
|
+
'- "catalog": optional. Set it to true for the full runner and model catalog \u2014 every model id with its label and thinking-effort variants, and every task mode with its description. It works before any task exists, and may be used on its own.',
|
|
4609
|
+
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
4610
|
+
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
4611
|
+
];
|
|
4612
|
+
var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
|
|
4613
|
+
function findTask(tasks, ref) {
|
|
4614
|
+
const byId = tasks.find((t) => t.id === ref);
|
|
4615
|
+
if (byId) return byId;
|
|
4616
|
+
const orderStr = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
4617
|
+
if (/^\d+$/.test(orderStr)) {
|
|
4618
|
+
const byOrder = tasks.filter((t) => t.order === Number(orderStr));
|
|
4619
|
+
if (byOrder.length === 1) return byOrder[0];
|
|
4620
|
+
}
|
|
4621
|
+
return tasks.find((t) => t.title === ref);
|
|
4622
|
+
}
|
|
4623
|
+
function renderField(task, field) {
|
|
4624
|
+
switch (field) {
|
|
4625
|
+
case "description":
|
|
4626
|
+
return [`description: ${task.description || "(none)"}`];
|
|
4627
|
+
case "prompt":
|
|
4628
|
+
return task.prompt ? ["prompt:", task.prompt] : ["prompt: (none)"];
|
|
4629
|
+
case "userSteps": {
|
|
4630
|
+
const steps = task.userSteps ?? [];
|
|
4631
|
+
if (steps.length === 0) return ["userSteps: (none)"];
|
|
4632
|
+
return ["userSteps:", ...steps.map((s) => ` ${s.order}. [${s.completed ? "x" : " "}] ${s.instruction}`)];
|
|
4633
|
+
}
|
|
4634
|
+
case "verdict": {
|
|
4635
|
+
const v = task.verdict;
|
|
4636
|
+
if (!v) return ["verdict: (none)"];
|
|
4637
|
+
const checks = v.checks.map((c) => `${c.name}=${c.skipped ? "skipped" : c.passed ? "pass" : "fail"}`).join(", ");
|
|
4638
|
+
return [`verdict: ${v.outcome.toUpperCase()} \u2014 ${v.reason}${checks ? ` [${checks}]` : ""}`];
|
|
4639
|
+
}
|
|
4640
|
+
case "outputSummary": {
|
|
4641
|
+
const o = task.outputSummary;
|
|
4642
|
+
if (!o) return ["outputSummary: (none)"];
|
|
4643
|
+
return [`outputSummary: ${o.reviewReason}`, ...o.logTail ? ["log tail:", o.logTail] : []];
|
|
4644
|
+
}
|
|
4645
|
+
case "userStoriesCovered": {
|
|
4646
|
+
const stories = task.userStoriesCovered ?? [];
|
|
4647
|
+
if (stories.length === 0) return ["userStoriesCovered: (none)"];
|
|
4648
|
+
return ["userStoriesCovered:", ...stories.map((s) => ` - ${s}`)];
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
function renderTask(task, fields) {
|
|
4653
|
+
return [
|
|
4654
|
+
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
4655
|
+
...fields.flatMap((f) => renderField(task, f)),
|
|
4656
|
+
""
|
|
4657
|
+
];
|
|
4658
|
+
}
|
|
4659
|
+
function renderCatalog(catalog) {
|
|
4660
|
+
const lines = [];
|
|
4661
|
+
for (const runner of catalog.runners) {
|
|
4662
|
+
const models = catalog.models[runner] ?? [];
|
|
4663
|
+
lines.push(`${runner} models:`);
|
|
4664
|
+
if (models.length === 0) lines.push(" (none discovered)");
|
|
4665
|
+
for (const m of models) {
|
|
4666
|
+
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
4667
|
+
lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
4668
|
+
}
|
|
4669
|
+
const modes = catalog.modes[runner] ?? [];
|
|
4670
|
+
lines.push(`${runner} task modes:`);
|
|
4671
|
+
if (modes.length === 0) lines.push(" (none declared)");
|
|
4672
|
+
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
4673
|
+
for (const mode of modes) {
|
|
4674
|
+
lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
return lines;
|
|
4678
|
+
}
|
|
4679
|
+
function renderTaskQueryAnswer(query, tasks, catalog) {
|
|
4680
|
+
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
4681
|
+
const blocks = [];
|
|
4682
|
+
if (query.tasks.length > 0) {
|
|
4683
|
+
const body = [];
|
|
4684
|
+
for (const ref of query.tasks) {
|
|
4685
|
+
const task = findTask(tasks, ref);
|
|
4686
|
+
if (!task) {
|
|
4687
|
+
body.push(`${ref}: no task matches this reference in the current plan.`, "");
|
|
4688
|
+
continue;
|
|
4689
|
+
}
|
|
4690
|
+
body.push(...renderTask(task, fields));
|
|
4691
|
+
}
|
|
4692
|
+
blocks.push(["<task_detail>", ...body, "</task_detail>"].join("\n"));
|
|
4693
|
+
}
|
|
4694
|
+
if (query.catalog) {
|
|
4695
|
+
blocks.push(["<runner_catalog>", ...renderCatalog(catalog), "</runner_catalog>"].join("\n"));
|
|
4696
|
+
}
|
|
4697
|
+
blocks.push(
|
|
4698
|
+
"That is everything you asked for. Nothing has changed. Reply now with the taskOps JSON for the edit you intended, or answer the user in prose."
|
|
4699
|
+
);
|
|
4700
|
+
return blocks.join("\n\n");
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4363
4703
|
// src/services/PlanPrompts.ts
|
|
4364
|
-
function buildResearchToolsPrompt(
|
|
4704
|
+
function buildResearchToolsPrompt() {
|
|
4365
4705
|
const lines = [
|
|
4366
4706
|
"You have access to the following tools to explore the workspace:",
|
|
4367
4707
|
"",
|
|
@@ -4377,15 +4717,11 @@ function buildResearchToolsPrompt(subagentsEnabled = false) {
|
|
|
4377
4717
|
"- glob: for finding files by name/path pattern",
|
|
4378
4718
|
"- grep: for searching inside file contents",
|
|
4379
4719
|
"",
|
|
4380
|
-
"When multiple independent tool calls are needed, batch them in a single response to save round-trips. For example, if you need to read README.md and AGENTS.md, call read_files with both paths at once."
|
|
4720
|
+
"When multiple independent tool calls are needed, batch them in a single response to save round-trips. For example, if you need to read README.md and AGENTS.md, call read_files with both paths at once.",
|
|
4721
|
+
"",
|
|
4722
|
+
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
4723
|
+
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
4381
4724
|
];
|
|
4382
|
-
if (subagentsEnabled) {
|
|
4383
|
-
lines.push(
|
|
4384
|
-
"",
|
|
4385
|
-
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
4386
|
-
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
4387
|
-
);
|
|
4388
|
-
}
|
|
4389
4725
|
return lines.join("\n");
|
|
4390
4726
|
}
|
|
4391
4727
|
function buildSubagentSystemPrompt() {
|
|
@@ -4423,11 +4759,11 @@ function buildModeExamplesForRunners(runners) {
|
|
|
4423
4759
|
return `${r}: default|plan`;
|
|
4424
4760
|
}).join(", ");
|
|
4425
4761
|
}
|
|
4426
|
-
function verificationModeBlock(
|
|
4762
|
+
function verificationModeBlock() {
|
|
4427
4763
|
return [
|
|
4428
4764
|
"",
|
|
4429
4765
|
"VERIFICATION MODE:",
|
|
4430
|
-
|
|
4766
|
+
"Add a FINAL verification task to the end of the plan (highest order number).",
|
|
4431
4767
|
'This task closes the gap between "every task finished" and "the feature is correct": tasks executed in isolated sessions can each pass while the integrated feature is still short. Its outcome must come from commands and exit codes, never from judgement.',
|
|
4432
4768
|
"This task must:",
|
|
4433
4769
|
'- Have type "ai" and autonomy "AFK" \u2014 it needs no human input',
|
|
@@ -4449,7 +4785,8 @@ function researchPhaseBlock(harnessMode) {
|
|
|
4449
4785
|
];
|
|
4450
4786
|
const toolSpecific = harnessMode ? [
|
|
4451
4787
|
"- Explore the repository with your own tools until you understand its architecture",
|
|
4452
|
-
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace"
|
|
4788
|
+
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
4789
|
+
"- If you delegate exploration to your own agents, WAIT for their results inside this reply. Do NOT launch them in the background or async and end your turn saying you will report back later: your turn ending is what hands the conversation back to the user, and anything you say after it never reaches them."
|
|
4453
4790
|
] : [
|
|
4454
4791
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
4455
4792
|
"- Use glob and grep to find relevant source files",
|
|
@@ -4462,35 +4799,14 @@ function researchPhaseBlock(harnessMode) {
|
|
|
4462
4799
|
"- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
|
|
4463
4800
|
].join("\n");
|
|
4464
4801
|
}
|
|
4465
|
-
function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, runnerModes, autonomousDefault = true,
|
|
4802
|
+
function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, runnerModes, autonomousDefault = true, verificationEnabled = false, harnessMode = false) {
|
|
4466
4803
|
const modelsJson = modelsJsonFor(modelsByRunner, runners);
|
|
4467
4804
|
const modeGuide = runnerModes ? buildModeGuide(runnerModes, autonomousDefault) : buildModeGuideForRunners(runners, autonomousDefault);
|
|
4468
4805
|
const modeExamples = runnerModes ? Object.entries(runnerModes).map(([r, ms]) => {
|
|
4469
4806
|
const filtered = filteredBuildModes(ms, autonomousDefault);
|
|
4470
4807
|
return `${r}: ${filtered.map((m) => m.id).join("|")}${ms.some((m) => m.id === "plan") ? "|plan" : ""}`;
|
|
4471
4808
|
}).join(", ") : buildModeExamplesForRunners(runners);
|
|
4472
|
-
const
|
|
4473
|
-
"",
|
|
4474
|
-
"INTERVIEW MODE \u2014 GRILL-ME:",
|
|
4475
|
-
"Interview the user relentlessly about every aspect of their goal until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.",
|
|
4476
|
-
"Ask one question at a time, waiting for the user's answer before continuing. For each question, propose your recommended answer so they can confirm or correct it. Asking multiple questions at once is bewildering.",
|
|
4477
|
-
"If a fact can be found by exploring the codebase, look it up instead of asking. The decisions, though, are the user's \u2014 put each one to them and wait.",
|
|
4478
|
-
"Only propose an outline after the user has answered enough questions that you genuinely understand the goal. Then present the prose outline and wait for explicit confirmation before emitting the task plan JSON."
|
|
4479
|
-
].join("\n") : "";
|
|
4480
|
-
const prdBlock = prdEnabled ? [
|
|
4481
|
-
"",
|
|
4482
|
-
"PRD MODE: Before producing the outline, generate a PRD in two steps.",
|
|
4483
|
-
"Step 1 \u2014 Propose a short prose preview containing: the problem statement, the proposed approach, the testing seams, and the main risks. Prefer existing seams over new ones; propose new seams at the highest point possible. The fewer seams across the codebase, the better \u2014 the ideal number is one. Propose a feature-slug for saving the PRD (e.g. `my-feature`). Ask the user whether they agree.",
|
|
4484
|
-
"Step 2 \u2014 Only after the user explicitly agrees, write the full markdown PRD using the standard PRD format (problem, user stories, implementation decisions, testing seams, out of scope). The user may edit the proposed feature-slug before you write the PRD; use the agreed slug in the final output.",
|
|
4485
|
-
'PRD content rules: user stories are an extensive numbered list, each "As an <actor>, I want <feature>, so that <benefit>". Record implementation decisions as decisions (modules, interfaces, schema changes, API contracts) \u2014 do NOT include specific file paths or code snippets; they go stale fast. Exception: a snippet from a prototype that encodes a decision more precisely than prose (state machine, reducer, schema, type shape) may be inlined, trimmed to the decision-rich parts.',
|
|
4486
|
-
"When writing the full PRD, wrap it EXACTLY with these markers so the system can save it automatically:",
|
|
4487
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" -->',
|
|
4488
|
-
"... full markdown PRD ...",
|
|
4489
|
-
"<!-- ORDEWELL_PRD_END -->",
|
|
4490
|
-
"Do NOT emit the full PRD before the user agrees to the preview. Do NOT emit task plan JSON before the PRD is agreed and the outline is confirmed."
|
|
4491
|
-
].join("\n") : "";
|
|
4492
|
-
const reviewBlock = reviewEnabled ? reviewModeBlock() : "";
|
|
4493
|
-
const verificationBlock = verificationEnabled ? verificationModeBlock(reviewEnabled) : "";
|
|
4809
|
+
const verificationBlock = verificationEnabled ? verificationModeBlock() : "";
|
|
4494
4810
|
return buildConversationBody(
|
|
4495
4811
|
goal,
|
|
4496
4812
|
context,
|
|
@@ -4499,34 +4815,10 @@ function buildConversationSystemPrompt(goal, context, modelsByRunner, runners, r
|
|
|
4499
4815
|
modeGuide,
|
|
4500
4816
|
modeExamples,
|
|
4501
4817
|
harnessMode,
|
|
4502
|
-
grillMeBlock,
|
|
4503
|
-
prdBlock,
|
|
4504
|
-
reviewBlock,
|
|
4505
4818
|
verificationBlock
|
|
4506
4819
|
);
|
|
4507
4820
|
}
|
|
4508
|
-
function
|
|
4509
|
-
return [
|
|
4510
|
-
"",
|
|
4511
|
-
"REVIEW MODE:",
|
|
4512
|
-
"Add a FINAL review task to the end of the plan (highest order number).",
|
|
4513
|
-
"This task must:",
|
|
4514
|
-
'- Have type "ai" and use the STRONGEST available model',
|
|
4515
|
-
"- Have dependencies on ALL other AI/user tasks in the plan",
|
|
4516
|
-
"- Be assigned appropriately as HITL (human review) since the results need human sign-off",
|
|
4517
|
-
"- Its prompt should instruct the agent to:",
|
|
4518
|
-
" * Review every completed task's outputs and verify the original goal was met",
|
|
4519
|
-
" * Check that each task delivers what its description promised",
|
|
4520
|
-
" * Review the work on two axes, reported SEPARATELY so one cannot mask the other:",
|
|
4521
|
-
" - SPEC axis: if a PRD exists for this plan (`.scratch/<slug>/PRD.md`), verify against it: (a) requirements asked for but missing or partial; (b) behaviour delivered that the PRD never asked for (scope creep); (c) requirements that look implemented but wrongly. Quote the PRD line for each finding. With no PRD, review against the original goal.",
|
|
4522
|
-
" - STANDARDS axis: check the diff against the repo's documented coding standards (CONTRIBUTING.md, CLAUDE.md, etc.) plus this smell baseline: Mysterious Name (rename), Duplicated Code (extract shared shape), Feature Envy (move method to the data), Data Clumps (bundle into a type), Primitive Obsession (give the concept a type), Repeated Switches (polymorphism or shared map), Shotgun Surgery (gather into one module), Divergent Change (split by reason to change), Speculative Generality (delete it), Message Chains (hide the walk), Middle Man (call the target direct), Refused Bequest (composition over inheritance). Smell findings are always judgement calls, never hard failures; documented repo standards override the baseline; skip anything tooling already enforces.",
|
|
4523
|
-
" * Produce a structured review report listing what was checked and whether each check passed",
|
|
4524
|
-
" * Flag any gaps, bugs, or incomplete work",
|
|
4525
|
-
" * Output a final PASS/FAIL conclusion with evidence (standards-axis judgement calls alone must not fail the verification)",
|
|
4526
|
-
"- Use the same runner as the other tasks"
|
|
4527
|
-
].join("\n");
|
|
4528
|
-
}
|
|
4529
|
-
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, harnessMode, grillMeBlock, prdBlock, reviewBlock, verificationBlock) {
|
|
4821
|
+
function buildConversationBody(goal, context, modelsJson, runners, modeGuide, modeExamples, harnessMode, verificationBlock) {
|
|
4530
4822
|
return [
|
|
4531
4823
|
"You are Ordewell's project planner. You explore the codebase, ask concise clarifying questions grounded in your findings, and produce structured task plans as JSON. Be direct: avoid unnecessary preamble, summaries, or explanations unless the user asks for detail.",
|
|
4532
4824
|
"",
|
|
@@ -4537,9 +4829,6 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
4537
4829
|
'4. After the user confirms the outline, emit the final task plan as a JSON object with a "tasks" array.',
|
|
4538
4830
|
"",
|
|
4539
4831
|
researchPhaseBlock(harnessMode),
|
|
4540
|
-
grillMeBlock,
|
|
4541
|
-
prdBlock,
|
|
4542
|
-
reviewBlock,
|
|
4543
4832
|
verificationBlock,
|
|
4544
4833
|
"",
|
|
4545
4834
|
"OUTLINE PHASE:",
|
|
@@ -4614,6 +4903,11 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
4614
4903
|
"TASK MODE:",
|
|
4615
4904
|
modeGuide,
|
|
4616
4905
|
"",
|
|
4906
|
+
// Shared by both variants on purpose: the read channel is a text envelope
|
|
4907
|
+
// exactly so a harness planner, which Ordewell cannot hand tools to, speaks
|
|
4908
|
+
// the same protocol as an API-backed one (ADR-0009).
|
|
4909
|
+
...TASK_QUERY_PROTOCOL,
|
|
4910
|
+
"",
|
|
4617
4911
|
context ? `PROJECT CONTEXT:
|
|
4618
4912
|
${context}
|
|
4619
4913
|
` : "",
|
|
@@ -4721,7 +5015,6 @@ function getPlanTemplate() {
|
|
|
4721
5015
|
"",
|
|
4722
5016
|
"TASK MODE:",
|
|
4723
5017
|
"{{MODE_GUIDE}}",
|
|
4724
|
-
"{{REVIEW_BLOCK}}",
|
|
4725
5018
|
"{{VERIFICATION_BLOCK}}",
|
|
4726
5019
|
"",
|
|
4727
5020
|
"CONTEXT:",
|
|
@@ -4768,10 +5061,8 @@ function buildPlanPromptBase(userGoal, context, researchResults, modelsByRunner,
|
|
|
4768
5061
|
const filtered = filteredBuildModes(ms, autonomousDefault);
|
|
4769
5062
|
return `${r}: ${filtered.map((m) => m.id).join("|")}${ms.some((m) => m.id === "plan") ? "|plan" : ""}`;
|
|
4770
5063
|
}).join(", ") : buildModeExamplesForRunners(runners);
|
|
4771
|
-
return template.replace("{{RESEARCH_SECTION}}", researchReplacement).replace("{{CLARIFICATION_INSTRUCTIONS}}", ONE_SHOT_INSTRUCTIONS).replace("{{MODELS_JSON}}", modelsJson).replace("{{RUNNER_CHOICES}}", runners.join("|")).replace("{{RUNNER_INSTRUCTION}}", runnerInstruction(runners)).replace("{{MODE_GUIDE}}", modeGuide).replace("{{
|
|
4772
|
-
${
|
|
4773
|
-
` : "").replace("{{VERIFICATION_BLOCK}}", scoped.verification ? `
|
|
4774
|
-
${verificationModeBlock(scoped.review)}
|
|
5064
|
+
return template.replace("{{RESEARCH_SECTION}}", researchReplacement).replace("{{CLARIFICATION_INSTRUCTIONS}}", ONE_SHOT_INSTRUCTIONS).replace("{{MODELS_JSON}}", modelsJson).replace("{{RUNNER_CHOICES}}", runners.join("|")).replace("{{RUNNER_INSTRUCTION}}", runnerInstruction(runners)).replace("{{MODE_GUIDE}}", modeGuide).replace("{{VERIFICATION_BLOCK}}", scoped.verification ? `
|
|
5065
|
+
${verificationModeBlock()}
|
|
4775
5066
|
` : "").replace("{{MODE_EXAMPLES}}", modeExamples).replace("{{CONTEXT}}", context || "(no additional context)").replace("{{RESEARCH_RESULTS}}", researchResults ? `
|
|
4776
5067
|
|
|
4777
5068
|
RESEARCH FINDINGS:
|
|
@@ -4921,7 +5212,8 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
4921
5212
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
4922
5213
|
'Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models listed in <available_models> above. Prefer the strongest model if the merged work is complex.',
|
|
4923
5214
|
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
4924
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}
|
|
5215
|
+
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
5216
|
+
`If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
|
|
4925
5217
|
].join("\n");
|
|
4926
5218
|
}
|
|
4927
5219
|
function buildSplitPrompt(taskId, tasks) {
|
|
@@ -4933,153 +5225,11 @@ function buildSplitPrompt(taskId, tasks) {
|
|
|
4933
5225
|
"The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
|
|
4934
5226
|
'Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models listed in <available_models> above. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).',
|
|
4935
5227
|
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
4936
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}
|
|
5228
|
+
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
5229
|
+
`If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
|
|
4937
5230
|
].join("\n");
|
|
4938
5231
|
}
|
|
4939
5232
|
|
|
4940
|
-
// src/services/JsonExtractor.ts
|
|
4941
|
-
var PLAN_ENVELOPE_KEY = "tasks";
|
|
4942
|
-
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
4943
|
-
var PlanParseError = class extends Error {
|
|
4944
|
-
raw;
|
|
4945
|
-
/**
|
|
4946
|
-
* True when the reply was cut off mid-object (output-token limit) rather
|
|
4947
|
-
* than malformed — the two need different repairs: a truncated emission will
|
|
4948
|
-
* be cut at the same point again unless the retry frees context or asks for
|
|
4949
|
-
* terser output.
|
|
4950
|
-
*/
|
|
4951
|
-
truncated;
|
|
4952
|
-
/**
|
|
4953
|
-
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
4954
|
-
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
4955
|
-
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
4956
|
-
* the rule it broke.
|
|
4957
|
-
*/
|
|
4958
|
-
semantic;
|
|
4959
|
-
constructor(message, raw, opts) {
|
|
4960
|
-
super(message);
|
|
4961
|
-
this.name = "PlanParseError";
|
|
4962
|
-
this.raw = raw;
|
|
4963
|
-
this.truncated = opts?.truncated ?? false;
|
|
4964
|
-
this.semantic = opts?.semantic ?? false;
|
|
4965
|
-
}
|
|
4966
|
-
};
|
|
4967
|
-
function stripModelNoise(raw) {
|
|
4968
|
-
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
4969
|
-
}
|
|
4970
|
-
function balancedObjectAt(text, start) {
|
|
4971
|
-
let depth = 0;
|
|
4972
|
-
let inString = false;
|
|
4973
|
-
let escaped = false;
|
|
4974
|
-
for (let i = start; i < text.length; i++) {
|
|
4975
|
-
const ch = text[i];
|
|
4976
|
-
if (inString) {
|
|
4977
|
-
if (escaped) escaped = false;
|
|
4978
|
-
else if (ch === "\\") escaped = true;
|
|
4979
|
-
else if (ch === '"') inString = false;
|
|
4980
|
-
continue;
|
|
4981
|
-
}
|
|
4982
|
-
if (ch === '"') inString = true;
|
|
4983
|
-
else if (ch === "{") depth++;
|
|
4984
|
-
else if (ch === "}") {
|
|
4985
|
-
depth--;
|
|
4986
|
-
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
|
-
return { json: text.slice(start), balanced: false };
|
|
4990
|
-
}
|
|
4991
|
-
function extractObjectsWithKey(raw, key) {
|
|
4992
|
-
const stripped = stripModelNoise(raw);
|
|
4993
|
-
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
4994
|
-
const matches = [];
|
|
4995
|
-
let firstObject = null;
|
|
4996
|
-
let sawBrace = false;
|
|
4997
|
-
let sawUnbalanced = false;
|
|
4998
|
-
let inString = false;
|
|
4999
|
-
let escaped = false;
|
|
5000
|
-
for (let i = 0; i < stripped.length; i++) {
|
|
5001
|
-
const ch = stripped[i];
|
|
5002
|
-
if (inString) {
|
|
5003
|
-
if (escaped) escaped = false;
|
|
5004
|
-
else if (ch === "\\") escaped = true;
|
|
5005
|
-
else if (ch === '"') inString = false;
|
|
5006
|
-
continue;
|
|
5007
|
-
}
|
|
5008
|
-
if (ch === '"') {
|
|
5009
|
-
inString = true;
|
|
5010
|
-
continue;
|
|
5011
|
-
}
|
|
5012
|
-
if (ch !== "{") continue;
|
|
5013
|
-
sawBrace = true;
|
|
5014
|
-
const obj = balancedObjectAt(stripped, i);
|
|
5015
|
-
if (obj.balanced && keyRe.test(obj.json)) {
|
|
5016
|
-
matches.push(obj.json);
|
|
5017
|
-
i += obj.json.length - 1;
|
|
5018
|
-
continue;
|
|
5019
|
-
}
|
|
5020
|
-
if (!obj.balanced) sawUnbalanced = true;
|
|
5021
|
-
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
5022
|
-
}
|
|
5023
|
-
return {
|
|
5024
|
-
matches,
|
|
5025
|
-
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
5026
|
-
sawBrace,
|
|
5027
|
-
sawUnbalanced
|
|
5028
|
-
};
|
|
5029
|
-
}
|
|
5030
|
-
function extractObjectWithBalance(raw) {
|
|
5031
|
-
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
5032
|
-
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
5033
|
-
return { ...fallback, sawBrace };
|
|
5034
|
-
}
|
|
5035
|
-
function extractJsonObject(raw) {
|
|
5036
|
-
return extractObjectWithBalance(raw).json;
|
|
5037
|
-
}
|
|
5038
|
-
function stripTrailingCommas(json) {
|
|
5039
|
-
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
5040
|
-
}
|
|
5041
|
-
function escapeControlCharsInStrings(json) {
|
|
5042
|
-
let out = "";
|
|
5043
|
-
let inString = false;
|
|
5044
|
-
let escaped = false;
|
|
5045
|
-
for (const ch of json) {
|
|
5046
|
-
if (inString) {
|
|
5047
|
-
if (escaped) {
|
|
5048
|
-
out += ch;
|
|
5049
|
-
escaped = false;
|
|
5050
|
-
continue;
|
|
5051
|
-
}
|
|
5052
|
-
if (ch === "\\") {
|
|
5053
|
-
out += ch;
|
|
5054
|
-
escaped = true;
|
|
5055
|
-
continue;
|
|
5056
|
-
}
|
|
5057
|
-
if (ch === '"') {
|
|
5058
|
-
out += ch;
|
|
5059
|
-
inString = false;
|
|
5060
|
-
continue;
|
|
5061
|
-
}
|
|
5062
|
-
if (ch === "\n") {
|
|
5063
|
-
out += "\\n";
|
|
5064
|
-
continue;
|
|
5065
|
-
}
|
|
5066
|
-
if (ch === "\r") {
|
|
5067
|
-
out += "\\r";
|
|
5068
|
-
continue;
|
|
5069
|
-
}
|
|
5070
|
-
if (ch === " ") {
|
|
5071
|
-
out += "\\t";
|
|
5072
|
-
continue;
|
|
5073
|
-
}
|
|
5074
|
-
out += ch;
|
|
5075
|
-
continue;
|
|
5076
|
-
}
|
|
5077
|
-
if (ch === '"') inString = true;
|
|
5078
|
-
out += ch;
|
|
5079
|
-
}
|
|
5080
|
-
return out;
|
|
5081
|
-
}
|
|
5082
|
-
|
|
5083
5233
|
// src/services/PlanValidator.ts
|
|
5084
5234
|
function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
|
|
5085
5235
|
const { matches, fallback, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
@@ -5320,6 +5470,171 @@ function validatePlanModification(ctx) {
|
|
|
5320
5470
|
return { valid: allErrors.length === 0, errors: allErrors };
|
|
5321
5471
|
}
|
|
5322
5472
|
|
|
5473
|
+
// src/services/TaskOps.ts
|
|
5474
|
+
var import_uuid2 = require("uuid");
|
|
5475
|
+
|
|
5476
|
+
// src/services/ModelAllowlistResolver.ts
|
|
5477
|
+
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
5478
|
+
if (!allowlist || allowlist.length === 0) return void 0;
|
|
5479
|
+
if (!modelsByRunner) return allowlist;
|
|
5480
|
+
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
5481
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
5482
|
+
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
5483
|
+
if (other === runner) continue;
|
|
5484
|
+
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
5485
|
+
}
|
|
5486
|
+
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
5487
|
+
return kept.length > 0 ? kept : void 0;
|
|
5488
|
+
}
|
|
5489
|
+
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
5490
|
+
const result = {};
|
|
5491
|
+
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
5492
|
+
if (!models) continue;
|
|
5493
|
+
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5494
|
+
if (!allowed) {
|
|
5495
|
+
result[runner] = models;
|
|
5496
|
+
continue;
|
|
5497
|
+
}
|
|
5498
|
+
const allowedSet = new Set(allowed);
|
|
5499
|
+
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
5500
|
+
const covered = new Set(intersection.map((m) => m.modelId));
|
|
5501
|
+
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
5502
|
+
result[runner] = [...intersection, ...missing];
|
|
5503
|
+
}
|
|
5504
|
+
return result;
|
|
5505
|
+
}
|
|
5506
|
+
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
5507
|
+
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
5508
|
+
function clampThinkingEffort(effort, variants) {
|
|
5509
|
+
if (!effort) return void 0;
|
|
5510
|
+
if (variants.length === 0) return void 0;
|
|
5511
|
+
const ids = new Set(variants.map((v) => v.id));
|
|
5512
|
+
if (ids.has(effort)) return effort;
|
|
5513
|
+
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
5514
|
+
if (ids.has(canon)) return canon;
|
|
5515
|
+
const idx = EFFORT_LADDER.indexOf(canon);
|
|
5516
|
+
if (idx < 0) return void 0;
|
|
5517
|
+
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
5518
|
+
const lower = EFFORT_LADDER[idx - d];
|
|
5519
|
+
if (lower && ids.has(lower)) return lower;
|
|
5520
|
+
const higher = EFFORT_LADDER[idx + d];
|
|
5521
|
+
if (higher && ids.has(higher)) return higher;
|
|
5522
|
+
}
|
|
5523
|
+
return void 0;
|
|
5524
|
+
}
|
|
5525
|
+
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
5526
|
+
const clampVariant = (task) => {
|
|
5527
|
+
if (!task.assignedModel || !modelsByRunner) return task;
|
|
5528
|
+
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
5529
|
+
(m) => m.modelId === task.assignedModel.modelId
|
|
5530
|
+
);
|
|
5531
|
+
if (!model) return task;
|
|
5532
|
+
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
5533
|
+
return {
|
|
5534
|
+
...task,
|
|
5535
|
+
assignedModel: {
|
|
5536
|
+
...task.assignedModel,
|
|
5537
|
+
thinkingEffort: clamped,
|
|
5538
|
+
availableVariants: model.variants.map((v) => v.id)
|
|
5539
|
+
}
|
|
5540
|
+
};
|
|
5541
|
+
};
|
|
5542
|
+
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5543
|
+
const assignmentFor = (runner, modelId) => {
|
|
5544
|
+
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
5545
|
+
return {
|
|
5546
|
+
modelId,
|
|
5547
|
+
modelLabel: known?.modelLabel ?? modelId,
|
|
5548
|
+
thinkingEffort: void 0,
|
|
5549
|
+
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
5550
|
+
};
|
|
5551
|
+
};
|
|
5552
|
+
return tasks.map((task) => {
|
|
5553
|
+
if (task.type === "user") return task;
|
|
5554
|
+
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
5555
|
+
const fallbackRunner = allowedRunners[0];
|
|
5556
|
+
const fallbackModels = allowedFor(fallbackRunner);
|
|
5557
|
+
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
5558
|
+
return clampVariant({
|
|
5559
|
+
...task,
|
|
5560
|
+
assignedRunner: fallbackRunner,
|
|
5561
|
+
assignedModel: fallbackModel
|
|
5562
|
+
});
|
|
5563
|
+
}
|
|
5564
|
+
const allowlist = allowedFor(task.assignedRunner);
|
|
5565
|
+
if (!task.assignedModel) {
|
|
5566
|
+
if (allowlist && allowlist.length > 0) {
|
|
5567
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
5568
|
+
}
|
|
5569
|
+
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
5570
|
+
if (discovered) {
|
|
5571
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
5572
|
+
}
|
|
5573
|
+
return task;
|
|
5574
|
+
}
|
|
5575
|
+
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
5576
|
+
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
5577
|
+
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
// src/services/TaskEditValidator.ts
|
|
5582
|
+
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
5583
|
+
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
5584
|
+
if (!catalog) return { ok: true };
|
|
5585
|
+
if (assignedModel) {
|
|
5586
|
+
const models = catalog.modelsByRunner[runner];
|
|
5587
|
+
if (models) {
|
|
5588
|
+
const modelId = assignedModel.modelId;
|
|
5589
|
+
if (!models.some((m) => m.modelId === modelId)) {
|
|
5590
|
+
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
5591
|
+
}
|
|
5592
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
5593
|
+
if (allowed && !allowed.includes(modelId)) {
|
|
5594
|
+
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
if (taskMode !== void 0) {
|
|
5599
|
+
const modes = catalog.runnerModes[runner];
|
|
5600
|
+
if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
|
|
5601
|
+
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
return { ok: true };
|
|
5605
|
+
}
|
|
5606
|
+
function typeCoherenceCheck(target, changes) {
|
|
5607
|
+
if (!changes.type || changes.type === target.type) return null;
|
|
5608
|
+
if (changes.type === "user") {
|
|
5609
|
+
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
5610
|
+
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
|
|
5611
|
+
}
|
|
5612
|
+
return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
|
|
5613
|
+
}
|
|
5614
|
+
if (!changes.prompt || !changes.prompt.trim()) {
|
|
5615
|
+
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
|
|
5616
|
+
}
|
|
5617
|
+
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
5618
|
+
}
|
|
5619
|
+
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
5620
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
5621
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
5622
|
+
if (actor === "planner") {
|
|
5623
|
+
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
|
|
5624
|
+
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
|
|
5625
|
+
}
|
|
5626
|
+
if (changes.dependencies) {
|
|
5627
|
+
const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
|
|
5628
|
+
if (!depCheck.ok) return depCheck;
|
|
5629
|
+
}
|
|
5630
|
+
const coherence = typeCoherenceCheck(target, changes);
|
|
5631
|
+
if (coherence && !coherence.ok) return coherence;
|
|
5632
|
+
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
5633
|
+
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
5634
|
+
if (!validity.ok) return validity;
|
|
5635
|
+
return coherence ?? { ok: true };
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5323
5638
|
// src/services/TaskOps.ts
|
|
5324
5639
|
var UPDATABLE_FIELDS = [
|
|
5325
5640
|
"title",
|
|
@@ -5332,7 +5647,8 @@ var UPDATABLE_FIELDS = [
|
|
|
5332
5647
|
"thinkingEffort",
|
|
5333
5648
|
"type",
|
|
5334
5649
|
"userSteps",
|
|
5335
|
-
"autonomy"
|
|
5650
|
+
"autonomy",
|
|
5651
|
+
"sliceType"
|
|
5336
5652
|
];
|
|
5337
5653
|
function textHasTaskOps(text) {
|
|
5338
5654
|
return text.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
@@ -5369,18 +5685,41 @@ function parseTaskOpsObject(json, text) {
|
|
|
5369
5685
|
}
|
|
5370
5686
|
return ops;
|
|
5371
5687
|
}
|
|
5372
|
-
function resolveRef(ref,
|
|
5373
|
-
if (typeof ref !== "string" && typeof ref !== "number") return
|
|
5688
|
+
function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
5689
|
+
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
5374
5690
|
const s = String(ref).trim();
|
|
5375
|
-
const
|
|
5376
|
-
if (
|
|
5691
|
+
const ownerIdx = handleOwner.get(s);
|
|
5692
|
+
if (ownerIdx !== void 0) {
|
|
5693
|
+
if (ownerIdx >= opIndex) {
|
|
5694
|
+
return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
|
|
5695
|
+
}
|
|
5696
|
+
const id = handleId.get(s);
|
|
5697
|
+
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
5698
|
+
return { id };
|
|
5699
|
+
}
|
|
5700
|
+
const byLiveId = liveScope.find((t) => t.id === s);
|
|
5701
|
+
if (byLiveId) return { id: byLiveId.id };
|
|
5377
5702
|
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
5378
5703
|
if (/^\d+$/.test(orderStr)) {
|
|
5379
5704
|
const order = Number(orderStr);
|
|
5380
|
-
const byOrder =
|
|
5381
|
-
if (byOrder.length === 1) return byOrder[0];
|
|
5705
|
+
const byOrder = originalScope.filter((t) => t.order === order);
|
|
5706
|
+
if (byOrder.length === 1) return { id: byOrder[0].id };
|
|
5382
5707
|
}
|
|
5383
|
-
|
|
5708
|
+
const byTitle = originalScope.find((t) => t.title === s);
|
|
5709
|
+
if (byTitle) return { id: byTitle.id };
|
|
5710
|
+
return { error: `task "${s}" not found` };
|
|
5711
|
+
}
|
|
5712
|
+
function refCollidesWithExisting(name, originalFlatAll) {
|
|
5713
|
+
if (originalFlatAll.some((t) => t.id === name)) return true;
|
|
5714
|
+
const orderStr = name.startsWith("#") ? name.slice(1) : name;
|
|
5715
|
+
if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
|
|
5716
|
+
return originalFlatAll.some((t) => t.title === name);
|
|
5717
|
+
}
|
|
5718
|
+
function findInTasks(tasks, id) {
|
|
5719
|
+
return flattenTasks(tasks).find((t) => t.id === id);
|
|
5720
|
+
}
|
|
5721
|
+
function findTopLevel(tasks, id) {
|
|
5722
|
+
return tasks.find((t) => t.id === id);
|
|
5384
5723
|
}
|
|
5385
5724
|
function detectCycle(tasks) {
|
|
5386
5725
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -5404,6 +5743,55 @@ function detectCycle(tasks) {
|
|
|
5404
5743
|
}
|
|
5405
5744
|
return null;
|
|
5406
5745
|
}
|
|
5746
|
+
function isPinned(t) {
|
|
5747
|
+
return t.status === "in_progress" || t.status === "completed";
|
|
5748
|
+
}
|
|
5749
|
+
function repairOrder(tasks) {
|
|
5750
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
5751
|
+
const placed = /* @__PURE__ */ new Set();
|
|
5752
|
+
const movable = tasks.filter((t) => !isPinned(t));
|
|
5753
|
+
const result = [];
|
|
5754
|
+
const blockerOf = (t) => {
|
|
5755
|
+
const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
|
|
5756
|
+
return id ? byId.get(id) : void 0;
|
|
5757
|
+
};
|
|
5758
|
+
const place = (t) => {
|
|
5759
|
+
result.push(t);
|
|
5760
|
+
placed.add(t.id);
|
|
5761
|
+
};
|
|
5762
|
+
for (const occupant of tasks) {
|
|
5763
|
+
if (isPinned(occupant)) {
|
|
5764
|
+
const blocker = blockerOf(occupant);
|
|
5765
|
+
if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
|
|
5766
|
+
place(occupant);
|
|
5767
|
+
continue;
|
|
5768
|
+
}
|
|
5769
|
+
const next = movable.findIndex((t) => !blockerOf(t));
|
|
5770
|
+
if (next < 0) {
|
|
5771
|
+
const [stuck, pin] = trailToPin(movable[0], blockerOf);
|
|
5772
|
+
return { error: pinnedRefusal(stuck, pin, "after") };
|
|
5773
|
+
}
|
|
5774
|
+
place(movable.splice(next, 1)[0]);
|
|
5775
|
+
}
|
|
5776
|
+
return { tasks: result };
|
|
5777
|
+
}
|
|
5778
|
+
function trailToPin(task, blockerOf) {
|
|
5779
|
+
let at = task;
|
|
5780
|
+
for (; ; ) {
|
|
5781
|
+
const blocker = blockerOf(at);
|
|
5782
|
+
if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
|
|
5783
|
+
at = blocker;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
function describeMoves(before, after) {
|
|
5787
|
+
const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
|
|
5788
|
+
const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
|
|
5789
|
+
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
5790
|
+
}
|
|
5791
|
+
function pinnedRefusal(other, pin, side) {
|
|
5792
|
+
const word = pin.status === "in_progress" ? "running" : "completed";
|
|
5793
|
+
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
5794
|
+
}
|
|
5407
5795
|
function canMergeTasks(tasks, selectedIds) {
|
|
5408
5796
|
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
5409
5797
|
const idSet = new Set(selectedIds);
|
|
@@ -5420,15 +5808,6 @@ function canMergeTasks(tasks, selectedIds) {
|
|
|
5420
5808
|
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
5421
5809
|
const cycle = detectCycle(sim);
|
|
5422
5810
|
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
5423
|
-
const orderById = new Map(sim.map((t) => [t.id, t.order]));
|
|
5424
|
-
for (const t of sim) {
|
|
5425
|
-
for (const dep of t.dependencies) {
|
|
5426
|
-
const depOrder = orderById.get(dep);
|
|
5427
|
-
if (depOrder !== void 0 && depOrder >= t.order) {
|
|
5428
|
-
return { ok: false, error: "Merging would break task ordering \u2014 a dependency of the merged set comes after it" };
|
|
5429
|
-
}
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5432
5811
|
return { ok: true };
|
|
5433
5812
|
}
|
|
5434
5813
|
function dependentsOf(tasks, taskId) {
|
|
@@ -5460,17 +5839,46 @@ function canSplitTask(tasks, taskId) {
|
|
|
5460
5839
|
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
5461
5840
|
return { ok: true };
|
|
5462
5841
|
}
|
|
5463
|
-
function applyTaskOps(currentTasks, ops, runners) {
|
|
5842
|
+
function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
5464
5843
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
5465
5844
|
const errors = [];
|
|
5466
5845
|
const summary = [];
|
|
5467
|
-
const
|
|
5846
|
+
const originalFlatAll = flattenTasks(currentTasks);
|
|
5847
|
+
const handleOwner = /* @__PURE__ */ new Map();
|
|
5848
|
+
const handleId = /* @__PURE__ */ new Map();
|
|
5849
|
+
for (const [i, op] of ops.entries()) {
|
|
5850
|
+
const handle = op.handle;
|
|
5851
|
+
if (handle === void 0) continue;
|
|
5852
|
+
if (typeof handle !== "string" || !handle.trim()) {
|
|
5853
|
+
errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
|
|
5854
|
+
continue;
|
|
5855
|
+
}
|
|
5856
|
+
if (handleOwner.has(handle)) {
|
|
5857
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
|
|
5858
|
+
continue;
|
|
5859
|
+
}
|
|
5860
|
+
if (refCollidesWithExisting(handle, originalFlatAll)) {
|
|
5861
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
|
|
5862
|
+
continue;
|
|
5863
|
+
}
|
|
5864
|
+
handleOwner.set(handle, i);
|
|
5865
|
+
}
|
|
5866
|
+
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
5867
|
+
const resolveDeps = (deps, opIndex) => {
|
|
5468
5868
|
const ids = [];
|
|
5469
5869
|
const bad = [];
|
|
5870
|
+
const liveFlat = flattenTasks(tasks);
|
|
5470
5871
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
5471
|
-
const
|
|
5472
|
-
if (
|
|
5473
|
-
|
|
5872
|
+
const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
5873
|
+
if ("error" in r) {
|
|
5874
|
+
bad.push(`${String(d)} (${r.error})`);
|
|
5875
|
+
continue;
|
|
5876
|
+
}
|
|
5877
|
+
if (!liveFlat.some((t) => t.id === r.id)) {
|
|
5878
|
+
bad.push(`${String(d)} (no longer exists in this batch)`);
|
|
5879
|
+
continue;
|
|
5880
|
+
}
|
|
5881
|
+
ids.push(r.id);
|
|
5474
5882
|
}
|
|
5475
5883
|
return { ids, bad };
|
|
5476
5884
|
};
|
|
@@ -5478,41 +5886,47 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5478
5886
|
const label = `op ${i + 1} (${op.op})`;
|
|
5479
5887
|
switch (op.op) {
|
|
5480
5888
|
case "update": {
|
|
5481
|
-
const
|
|
5482
|
-
if (
|
|
5483
|
-
errors.push(`${label}:
|
|
5484
|
-
break;
|
|
5485
|
-
}
|
|
5486
|
-
if (target.status === "in_progress") {
|
|
5487
|
-
errors.push(`${label}: "${target.title}" is running and cannot be modified`);
|
|
5889
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
5890
|
+
if ("error" in ref) {
|
|
5891
|
+
errors.push(`${label}: ${ref.error}`);
|
|
5488
5892
|
break;
|
|
5489
5893
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5894
|
+
const target = findInTasks(tasks, ref.id);
|
|
5895
|
+
if (!target) {
|
|
5896
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
5492
5897
|
break;
|
|
5493
5898
|
}
|
|
5494
5899
|
const changes = {};
|
|
5495
5900
|
for (const key of UPDATABLE_FIELDS) {
|
|
5496
5901
|
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
5497
5902
|
}
|
|
5903
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
5904
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
5905
|
+
break;
|
|
5906
|
+
}
|
|
5907
|
+
const changesForCheck = { ...changes };
|
|
5908
|
+
delete changesForCheck.dependencies;
|
|
5909
|
+
const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
5910
|
+
if (!check.ok) {
|
|
5911
|
+
errors.push(`${label}: ${check.error}`);
|
|
5912
|
+
break;
|
|
5913
|
+
}
|
|
5914
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
5498
5915
|
if ("dependencies" in changes) {
|
|
5499
|
-
const { ids, bad } = resolveDeps(changes.dependencies);
|
|
5916
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
5500
5917
|
if (bad.length) {
|
|
5501
5918
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5502
5919
|
break;
|
|
5503
5920
|
}
|
|
5504
5921
|
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
5505
5922
|
}
|
|
5506
|
-
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
5507
|
-
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
5508
|
-
break;
|
|
5509
|
-
}
|
|
5510
5923
|
if (Object.keys(changes).length === 0) {
|
|
5511
5924
|
errors.push(`${label}: no valid changes provided`);
|
|
5512
5925
|
break;
|
|
5513
5926
|
}
|
|
5514
5927
|
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
5515
|
-
|
|
5928
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
5929
|
+
summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
|
|
5516
5930
|
break;
|
|
5517
5931
|
}
|
|
5518
5932
|
case "add": {
|
|
@@ -5521,27 +5935,39 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5521
5935
|
errors.push(`${label}: new task needs a title`);
|
|
5522
5936
|
break;
|
|
5523
5937
|
}
|
|
5524
|
-
const { ids, bad } = resolveDeps(spec.dependencies);
|
|
5938
|
+
const { ids, bad } = resolveDeps(spec.dependencies, i);
|
|
5525
5939
|
if (bad.length) {
|
|
5526
5940
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5527
5941
|
break;
|
|
5528
5942
|
}
|
|
5529
5943
|
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
|
|
5944
|
+
const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
|
|
5945
|
+
if (!validity.ok) {
|
|
5946
|
+
errors.push(`${label}: ${validity.error}`);
|
|
5947
|
+
break;
|
|
5948
|
+
}
|
|
5949
|
+
const newId = (0, import_uuid2.v4)();
|
|
5530
5950
|
tasks = addTaskToPlan(tasks, {
|
|
5531
5951
|
...spec,
|
|
5532
|
-
id:
|
|
5952
|
+
id: newId,
|
|
5533
5953
|
dependencies: ids,
|
|
5534
5954
|
assignedRunner: runner,
|
|
5535
5955
|
description: spec.description ?? spec.title,
|
|
5536
5956
|
prompt: spec.prompt ?? spec.description ?? spec.title
|
|
5537
5957
|
});
|
|
5958
|
+
if (op.handle) handleId.set(op.handle, newId);
|
|
5538
5959
|
summary.push(`Added "${spec.title}"`);
|
|
5539
5960
|
break;
|
|
5540
5961
|
}
|
|
5541
5962
|
case "remove": {
|
|
5542
|
-
const
|
|
5963
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
5964
|
+
if ("error" in ref) {
|
|
5965
|
+
errors.push(`${label}: ${ref.error}`);
|
|
5966
|
+
break;
|
|
5967
|
+
}
|
|
5968
|
+
const target = findInTasks(tasks, ref.id);
|
|
5543
5969
|
if (!target) {
|
|
5544
|
-
errors.push(`${label}: task "${op.taskId}"
|
|
5970
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
5545
5971
|
break;
|
|
5546
5972
|
}
|
|
5547
5973
|
if (target.status === "in_progress") {
|
|
@@ -5557,7 +5983,13 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5557
5983
|
break;
|
|
5558
5984
|
}
|
|
5559
5985
|
case "reorder": {
|
|
5560
|
-
const
|
|
5986
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
5987
|
+
const badRef = refs.find((r) => "error" in r);
|
|
5988
|
+
if (badRef) {
|
|
5989
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
5990
|
+
break;
|
|
5991
|
+
}
|
|
5992
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
5561
5993
|
if (resolved.some((t) => !t)) {
|
|
5562
5994
|
errors.push(`${label}: unknown task in reorder list`);
|
|
5563
5995
|
break;
|
|
@@ -5573,9 +6005,15 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5573
6005
|
break;
|
|
5574
6006
|
}
|
|
5575
6007
|
case "merge": {
|
|
5576
|
-
const
|
|
6008
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
6009
|
+
const badRef = refs.find((r) => "error" in r);
|
|
6010
|
+
if (badRef) {
|
|
6011
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
6012
|
+
break;
|
|
6013
|
+
}
|
|
6014
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
5577
6015
|
if (resolved.some((t) => !t)) {
|
|
5578
|
-
errors.push(`${label}: unknown task in merge list`);
|
|
6016
|
+
errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
|
|
5579
6017
|
break;
|
|
5580
6018
|
}
|
|
5581
6019
|
const toMerge = resolved.map((t) => t).sort((a, b) => a.order - b.order);
|
|
@@ -5590,7 +6028,7 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5590
6028
|
}
|
|
5591
6029
|
const idSet = new Set(toMerge.map((t) => t.id));
|
|
5592
6030
|
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
5593
|
-
const { ids, bad } = resolveDeps(unionDepsRaw);
|
|
6031
|
+
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
5594
6032
|
if (bad.length) {
|
|
5595
6033
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
5596
6034
|
break;
|
|
@@ -5631,11 +6069,17 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5631
6069
|
}
|
|
5632
6070
|
if (!inserted) result.push(mergedTask);
|
|
5633
6071
|
tasks = renumberTasks(result);
|
|
6072
|
+
if (op.handle) handleId.set(op.handle, mergedId);
|
|
5634
6073
|
summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
|
|
5635
6074
|
break;
|
|
5636
6075
|
}
|
|
5637
6076
|
case "split": {
|
|
5638
|
-
const
|
|
6077
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
6078
|
+
if ("error" in ref) {
|
|
6079
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6080
|
+
break;
|
|
6081
|
+
}
|
|
6082
|
+
const target = findTopLevel(tasks, ref.id);
|
|
5639
6083
|
if (!target) {
|
|
5640
6084
|
errors.push(`${label}: task "${op.taskId}" not found`);
|
|
5641
6085
|
break;
|
|
@@ -5656,16 +6100,16 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5656
6100
|
const newTasks = [];
|
|
5657
6101
|
let prevId;
|
|
5658
6102
|
let buildError = false;
|
|
5659
|
-
for (let
|
|
5660
|
-
const spec = parts[
|
|
6103
|
+
for (let p = 0; p < parts.length; p++) {
|
|
6104
|
+
const spec = parts[p];
|
|
5661
6105
|
if (!spec || !spec.title) {
|
|
5662
|
-
errors.push(`${label}: split part ${
|
|
6106
|
+
errors.push(`${label}: split part ${p + 1} needs a title`);
|
|
5663
6107
|
buildError = true;
|
|
5664
6108
|
break;
|
|
5665
6109
|
}
|
|
5666
6110
|
let deps;
|
|
5667
|
-
if (
|
|
5668
|
-
const { ids, bad } = resolveDeps(target.dependencies);
|
|
6111
|
+
if (p === 0) {
|
|
6112
|
+
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
5669
6113
|
if (bad.length) {
|
|
5670
6114
|
errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
|
|
5671
6115
|
buildError = true;
|
|
@@ -5703,9 +6147,60 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5703
6147
|
result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
|
|
5704
6148
|
}
|
|
5705
6149
|
tasks = renumberTasks(result);
|
|
6150
|
+
if (op.handle) handleId.set(op.handle, tailId);
|
|
5706
6151
|
summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
|
|
5707
6152
|
break;
|
|
5708
6153
|
}
|
|
6154
|
+
case "rearm": {
|
|
6155
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
6156
|
+
if ("error" in ref) {
|
|
6157
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6158
|
+
break;
|
|
6159
|
+
}
|
|
6160
|
+
const target = findInTasks(tasks, ref.id);
|
|
6161
|
+
if (!target) {
|
|
6162
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
6163
|
+
break;
|
|
6164
|
+
}
|
|
6165
|
+
if (target.status === "in_progress") {
|
|
6166
|
+
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
6167
|
+
break;
|
|
6168
|
+
}
|
|
6169
|
+
const changes = {};
|
|
6170
|
+
for (const key of UPDATABLE_FIELDS) {
|
|
6171
|
+
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
6172
|
+
}
|
|
6173
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
6174
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
6175
|
+
break;
|
|
6176
|
+
}
|
|
6177
|
+
const changesForCheck = { ...changes };
|
|
6178
|
+
delete changesForCheck.dependencies;
|
|
6179
|
+
const check = validateTaskEdit("direct", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
6180
|
+
if (!check.ok) {
|
|
6181
|
+
errors.push(`${label}: ${check.error}`);
|
|
6182
|
+
break;
|
|
6183
|
+
}
|
|
6184
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
6185
|
+
if ("dependencies" in changes) {
|
|
6186
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
6187
|
+
if (bad.length) {
|
|
6188
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
6189
|
+
break;
|
|
6190
|
+
}
|
|
6191
|
+
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
6192
|
+
}
|
|
6193
|
+
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0 });
|
|
6194
|
+
for (const dep of flattenTasks(tasks)) {
|
|
6195
|
+
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
6196
|
+
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
6200
|
+
const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
|
|
6201
|
+
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
6202
|
+
break;
|
|
6203
|
+
}
|
|
5709
6204
|
default:
|
|
5710
6205
|
errors.push(`${label}: unknown op "${op.op}"`);
|
|
5711
6206
|
}
|
|
@@ -5721,13 +6216,14 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
5721
6216
|
}
|
|
5722
6217
|
const cycleAt = detectCycle(flat);
|
|
5723
6218
|
if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
}
|
|
6219
|
+
if (!errors.length) {
|
|
6220
|
+
const repaired = repairOrder(tasks);
|
|
6221
|
+
if ("error" in repaired) {
|
|
6222
|
+
errors.push(repaired.error);
|
|
6223
|
+
} else {
|
|
6224
|
+
const moves = describeMoves(tasks, repaired.tasks);
|
|
6225
|
+
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
6226
|
+
tasks = renumberTasks(repaired.tasks);
|
|
5731
6227
|
}
|
|
5732
6228
|
}
|
|
5733
6229
|
}
|
|
@@ -5757,6 +6253,9 @@ function truncatedPlanReEmitPrompt(historyCompacted) {
|
|
|
5757
6253
|
function reEmitTaskOpsPrompt(detail) {
|
|
5758
6254
|
return `Your task edits could not be applied: ${detail}. Re-emit ONLY a corrected {"${TASK_OPS_ENVELOPE_KEY}":[...]} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not intend to edit tasks.`;
|
|
5759
6255
|
}
|
|
6256
|
+
function reEmitTaskQueryPrompt(detail) {
|
|
6257
|
+
return `Your task-detail request could not be read: ${detail}. Re-emit ONLY a corrected {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"fields"?:[...],"catalog"?:true}} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not mean to look anything up.`;
|
|
6258
|
+
}
|
|
5760
6259
|
function taskOpsRejectedPrompt(errors) {
|
|
5761
6260
|
return `Those task edits were rejected:
|
|
5762
6261
|
- ${errors.join("\n- ")}
|
|
@@ -5783,6 +6282,16 @@ function classifyPlannerReply(text, opts) {
|
|
|
5783
6282
|
}
|
|
5784
6283
|
}
|
|
5785
6284
|
}
|
|
6285
|
+
if (textHasTaskQuery(text)) {
|
|
6286
|
+
try {
|
|
6287
|
+
return { kind: "task_query", query: parseTaskQueryJson(text) };
|
|
6288
|
+
} catch (err) {
|
|
6289
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
6290
|
+
if (extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY).matches.length > 0) {
|
|
6291
|
+
return { kind: "broken_task_query", error: err };
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
5786
6295
|
if (text.includes(`"${PLAN_ENVELOPE_KEY}"`)) {
|
|
5787
6296
|
try {
|
|
5788
6297
|
return { kind: "plan", tasks: parsePlanJson(text, opts.runners, opts.runnerModes, opts.autonomousDefault) };
|
|
@@ -5916,8 +6425,8 @@ var SUBAGENT_EXCLUDED = /* @__PURE__ */ new Set(["fetch", "web_search"]);
|
|
|
5916
6425
|
function subagentToolSpecs() {
|
|
5917
6426
|
return RESEARCH_TOOLS.filter((t) => !SUBAGENT_EXCLUDED.has(t.name));
|
|
5918
6427
|
}
|
|
5919
|
-
function withSpawn(
|
|
5920
|
-
return
|
|
6428
|
+
function withSpawn() {
|
|
6429
|
+
return [...RESEARCH_TOOLS, SPAWN_RESEARCH_AGENT_SPEC];
|
|
5921
6430
|
}
|
|
5922
6431
|
function projectOpenAi(specs) {
|
|
5923
6432
|
return specs.map((t) => ({
|
|
@@ -5938,8 +6447,8 @@ function projectOpenAi(specs) {
|
|
|
5938
6447
|
}
|
|
5939
6448
|
}));
|
|
5940
6449
|
}
|
|
5941
|
-
function toOpenAiTools(
|
|
5942
|
-
return projectOpenAi(withSpawn(
|
|
6450
|
+
function toOpenAiTools() {
|
|
6451
|
+
return projectOpenAi(withSpawn());
|
|
5943
6452
|
}
|
|
5944
6453
|
function toOpenAiSubagentTools() {
|
|
5945
6454
|
return projectOpenAi(subagentToolSpecs());
|
|
@@ -5950,8 +6459,8 @@ var SCHEMA_TYPE = {
|
|
|
5950
6459
|
array: import_generative_ai.SchemaType.ARRAY,
|
|
5951
6460
|
boolean: import_generative_ai.SchemaType.BOOLEAN
|
|
5952
6461
|
};
|
|
5953
|
-
function toGeminiToolDeclarations(
|
|
5954
|
-
return withSpawn(
|
|
6462
|
+
function toGeminiToolDeclarations() {
|
|
6463
|
+
return withSpawn().map((t) => {
|
|
5955
6464
|
const properties = {};
|
|
5956
6465
|
for (const [key, param] of Object.entries(t.properties)) {
|
|
5957
6466
|
properties[key] = {
|
|
@@ -6048,33 +6557,10 @@ function compactResearchResults(resultsText, maxChars = COMPACTION_LIMITS.resear
|
|
|
6048
6557
|
return (blocks[0].lines.length === 0 ? texts.slice(1) : texts).join("\n");
|
|
6049
6558
|
}
|
|
6050
6559
|
|
|
6051
|
-
// src/utils/prdStore.ts
|
|
6052
|
-
var fs = __toESM(require("fs"));
|
|
6053
|
-
var path4 = __toESM(require("path"));
|
|
6054
|
-
var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
|
|
6055
|
-
function extractPrdBlock(text) {
|
|
6056
|
-
const match = PRD_BLOCK_RE.exec(text);
|
|
6057
|
-
if (!match) return null;
|
|
6058
|
-
const slug2 = sanitizeSlug(match[1]);
|
|
6059
|
-
const markdown = match[2].trim();
|
|
6060
|
-
if (!slug2 || !markdown) return null;
|
|
6061
|
-
return { slug: slug2, markdown };
|
|
6062
|
-
}
|
|
6063
|
-
function sanitizeSlug(raw) {
|
|
6064
|
-
return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
6065
|
-
}
|
|
6066
|
-
function savePrdMarkdown(workspace, slug2, markdown) {
|
|
6067
|
-
const dir = path4.join(workspace, ".scratch", sanitizeSlug(slug2));
|
|
6068
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
6069
|
-
const file = path4.join(dir, "PRD.md");
|
|
6070
|
-
fs.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
|
|
6071
|
-
return file;
|
|
6072
|
-
}
|
|
6073
|
-
|
|
6074
6560
|
// src/services/ContextCollector.ts
|
|
6075
6561
|
var ORDEWELL_CONTEXT_MAX = 8e3;
|
|
6076
|
-
async function collectResearchContext(
|
|
6077
|
-
const collector = new ContextCollector(
|
|
6562
|
+
async function collectResearchContext(fs12, runners) {
|
|
6563
|
+
const collector = new ContextCollector(fs12);
|
|
6078
6564
|
let contextStr = "";
|
|
6079
6565
|
let firstContext = null;
|
|
6080
6566
|
for (const r of runners) {
|
|
@@ -6101,8 +6587,8 @@ ${firstContext.dirStructure}
|
|
|
6101
6587
|
return contextStr;
|
|
6102
6588
|
}
|
|
6103
6589
|
var ContextCollector = class {
|
|
6104
|
-
constructor(
|
|
6105
|
-
this.fs =
|
|
6590
|
+
constructor(fs12, registry) {
|
|
6591
|
+
this.fs = fs12;
|
|
6106
6592
|
this.registry = registry;
|
|
6107
6593
|
}
|
|
6108
6594
|
fs;
|
|
@@ -6183,9 +6669,9 @@ function summarizeToolCall(tool, argsJson, toolLabel) {
|
|
|
6183
6669
|
const scope = args.include ? ` in ${args.include}` : "";
|
|
6184
6670
|
return `grep ${ellipsize(String(args.pattern), 40)}${scope}${mode}`;
|
|
6185
6671
|
}
|
|
6186
|
-
const
|
|
6187
|
-
if (
|
|
6188
|
-
const basename = String(
|
|
6672
|
+
const path20 = args.filePath || args.file_path || args.path;
|
|
6673
|
+
if (path20) {
|
|
6674
|
+
const basename = String(path20).split("/").pop() || String(path20);
|
|
6189
6675
|
return `${display} ${basename}`;
|
|
6190
6676
|
}
|
|
6191
6677
|
if (args.pattern) {
|
|
@@ -6219,22 +6705,22 @@ var ALLOWED_TOOL_NAMES = new Set(subagentToolSpecs().map((t) => t.name));
|
|
|
6219
6705
|
function refused(output) {
|
|
6220
6706
|
return { success: false, output, truncated: false };
|
|
6221
6707
|
}
|
|
6222
|
-
function nonPromptingFs(
|
|
6223
|
-
const withinWorkspace = (p) => resolveWithin(
|
|
6708
|
+
function nonPromptingFs(fs12) {
|
|
6709
|
+
const withinWorkspace = (p) => resolveWithin(fs12.getWorkspaceRoot(), p ?? ".").inside;
|
|
6224
6710
|
const outsideRefusal = (p) => refused(
|
|
6225
6711
|
`Path "${p}" is outside the workspace. Research agents are confined to the workspace root \u2014 note the gap in your digest and let the planner request it directly.`
|
|
6226
6712
|
);
|
|
6227
6713
|
return {
|
|
6228
|
-
getWorkspaceRoot: () =>
|
|
6229
|
-
readFile: (p, opts) => withinWorkspace(p) ?
|
|
6714
|
+
getWorkspaceRoot: () => fs12.getWorkspaceRoot(),
|
|
6715
|
+
readFile: (p, opts) => withinWorkspace(p) ? fs12.readFile(p, opts) : Promise.resolve(outsideRefusal(p)),
|
|
6230
6716
|
readFiles: (paths) => {
|
|
6231
6717
|
const escaping = paths.find((p) => !withinWorkspace(p));
|
|
6232
|
-
return escaping ? Promise.resolve(outsideRefusal(escaping)) :
|
|
6718
|
+
return escaping ? Promise.resolve(outsideRefusal(escaping)) : fs12.readFiles(paths);
|
|
6233
6719
|
},
|
|
6234
|
-
glob: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
6235
|
-
grep: (pattern, opts) => withinWorkspace(opts?.path) ?
|
|
6236
|
-
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ?
|
|
6237
|
-
listDir: (p, depth) => withinWorkspace(p) ?
|
|
6720
|
+
glob: (pattern, opts) => withinWorkspace(opts?.path) ? fs12.glob(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6721
|
+
grep: (pattern, opts) => withinWorkspace(opts?.path) ? fs12.grep(pattern, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6722
|
+
findSymbol: (symbol, opts) => withinWorkspace(opts?.path) ? fs12.findSymbol(symbol, opts) : Promise.resolve(outsideRefusal(opts.path)),
|
|
6723
|
+
listDir: (p, depth) => withinWorkspace(p) ? fs12.listDir(p, depth) : Promise.resolve(outsideRefusal(p)),
|
|
6238
6724
|
bash: async (command, signal) => {
|
|
6239
6725
|
const dialect = resolveResearchShell().dialect;
|
|
6240
6726
|
const { tier, reason } = classifyCommand(command, { dialect });
|
|
@@ -6245,13 +6731,13 @@ function nonPromptingFs(fs10) {
|
|
|
6245
6731
|
}
|
|
6246
6732
|
const escaping = pathLikeArgs(command, { dialect }).find((p) => !withinWorkspace(p));
|
|
6247
6733
|
if (escaping) return outsideRefusal(escaping);
|
|
6248
|
-
return
|
|
6734
|
+
return fs12.bash(command, signal);
|
|
6249
6735
|
}
|
|
6250
6736
|
};
|
|
6251
6737
|
}
|
|
6252
6738
|
async function runLoop(prompt, deps) {
|
|
6253
6739
|
if (deps.signal?.aborted) return "[research agent aborted before starting]";
|
|
6254
|
-
const
|
|
6740
|
+
const fs12 = nonPromptingFs(deps.fs);
|
|
6255
6741
|
const chat = deps.createChat((delta) => deps.onProgress?.({ type: "thinking", text: delta }));
|
|
6256
6742
|
let turn = await chat.sendMessage(prompt, deps.signal);
|
|
6257
6743
|
for (let step = 0; turn.hasToolCalls && step < SUBAGENT_LIMITS.maxSteps; step++) {
|
|
@@ -6268,7 +6754,7 @@ ${turn.text}`;
|
|
|
6268
6754
|
if (tc.name === "read_file" && !("maxBytes" in args) && !("limit" in args)) args.limit = 2e3;
|
|
6269
6755
|
const toolArgs = JSON.stringify(tc.args);
|
|
6270
6756
|
deps.onProgress?.({ type: "tool_call", tool: tc.name, toolArgs, toolCallId: tc.id });
|
|
6271
|
-
const res = await executeTool(tc.name, args,
|
|
6757
|
+
const res = await executeTool(tc.name, args, fs12, void 0, deps.signal);
|
|
6272
6758
|
const output = res.output.length > SUBAGENT_LIMITS.toolOutputMaxChars ? res.output.slice(0, SUBAGENT_LIMITS.toolOutputMaxChars) + `
|
|
6273
6759
|
[... truncated to ${SUBAGENT_LIMITS.toolOutputMaxChars} chars, total ${res.output.length}]` : res.output;
|
|
6274
6760
|
const stepEntry = {
|
|
@@ -6326,8 +6812,6 @@ var BaseAiService = class _BaseAiService {
|
|
|
6326
6812
|
config;
|
|
6327
6813
|
conversation = null;
|
|
6328
6814
|
activeAbort = null;
|
|
6329
|
-
/** researchSubagents toggle (issue #34): set per operation from the live settings snapshot; off means bit-for-bit sequential behavior. */
|
|
6330
|
-
researchSubagentsEnabled = false;
|
|
6331
6815
|
hasActiveConversation() {
|
|
6332
6816
|
return this.conversation !== null;
|
|
6333
6817
|
}
|
|
@@ -6383,10 +6867,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
6383
6867
|
* steering/failed result and the loop continues sequentially: a subagent can
|
|
6384
6868
|
* never fail a plan or a turn.
|
|
6385
6869
|
*/
|
|
6386
|
-
async executeSpawnAgent(args,
|
|
6387
|
-
if (!this.researchSubagentsEnabled) {
|
|
6388
|
-
return { success: false, output: "spawn_research_agent is not available in this session. Continue researching sequentially with your other tools (read_file, glob, grep, list_dir, bash).", truncated: false };
|
|
6389
|
-
}
|
|
6870
|
+
async executeSpawnAgent(args, fs12, onProgress, signal, subagentId) {
|
|
6390
6871
|
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
6391
6872
|
if (!prompt) {
|
|
6392
6873
|
return { success: false, output: 'spawn_research_agent requires a non-empty "prompt" string: a self-contained task for the agent, including what its digest must report back.', truncated: false };
|
|
@@ -6397,20 +6878,20 @@ var BaseAiService = class _BaseAiService {
|
|
|
6397
6878
|
if (!chat) throw new Error("subagent chats are not available for this provider");
|
|
6398
6879
|
return chat;
|
|
6399
6880
|
},
|
|
6400
|
-
fs:
|
|
6881
|
+
fs: fs12,
|
|
6401
6882
|
signal,
|
|
6402
6883
|
onProgress: (progress) => onProgress({ ...progress, subagentId })
|
|
6403
6884
|
});
|
|
6404
6885
|
}
|
|
6405
6886
|
/** Collect project context for the planning phase. Shared with the harness backend. */
|
|
6406
|
-
static async collectResearchContext(
|
|
6407
|
-
return collectResearchContext(
|
|
6887
|
+
static async collectResearchContext(fs12, runners) {
|
|
6888
|
+
return collectResearchContext(fs12, runners);
|
|
6408
6889
|
}
|
|
6409
6890
|
/**
|
|
6410
6891
|
* Execute one round of tool calls and report each through onProgress.
|
|
6411
6892
|
* Returns the results to feed back plus the log entries produced.
|
|
6412
6893
|
*/
|
|
6413
|
-
async executeToolCalls(toolCalls,
|
|
6894
|
+
async executeToolCalls(toolCalls, fs12, onProgress, fetcher, stepIndex, thinking, signal) {
|
|
6414
6895
|
const toolResults = [];
|
|
6415
6896
|
const logEntries = [];
|
|
6416
6897
|
let resultsText = "";
|
|
@@ -6426,21 +6907,21 @@ var BaseAiService = class _BaseAiService {
|
|
|
6426
6907
|
const parallelIndexes = toolCalls.flatMap((tc, i) => PARALLEL_SAFE_TOOLS.has(tc.name) ? [i] : []);
|
|
6427
6908
|
await Promise.all([
|
|
6428
6909
|
spawnIndexes.length > 0 ? mapWithConcurrency(spawnIndexes, SUBAGENT_LIMITS.maxConcurrent, async (i) => {
|
|
6429
|
-
const subagentId = (0,
|
|
6910
|
+
const subagentId = (0, import_uuid3.v4)();
|
|
6430
6911
|
spawnSubagentIds.set(i, subagentId);
|
|
6431
6912
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
6432
|
-
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args },
|
|
6913
|
+
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs12, onProgress, signal, subagentId));
|
|
6433
6914
|
}) : Promise.resolve(),
|
|
6434
6915
|
parallelIndexes.length > 1 ? mapWithConcurrency(parallelIndexes, TOOL_ROUND_CONCURRENCY, async (i) => {
|
|
6435
6916
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), toolCallId: toolCalls[i].id });
|
|
6436
|
-
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i],
|
|
6917
|
+
precomputed.set(i, await executeTool(toolCalls[i].name, normalizedArgs[i], fs12, fetcher, signal));
|
|
6437
6918
|
}) : Promise.resolve()
|
|
6438
6919
|
]);
|
|
6439
6920
|
for (const [index, tc] of toolCalls.entries()) {
|
|
6440
6921
|
const toolArgs = normalizedArgs[index];
|
|
6441
6922
|
const preResult = precomputed.get(index);
|
|
6442
6923
|
if (!preResult) onProgress({ type: "tool_call", tool: tc.name, toolArgs: JSON.stringify(tc.args), toolCallId: tc.id });
|
|
6443
|
-
const toolResult = preResult ?? await executeTool(tc.name, toolArgs,
|
|
6924
|
+
const toolResult = preResult ?? await executeTool(tc.name, toolArgs, fs12, fetcher, signal);
|
|
6444
6925
|
const MAX_RESPONSE_CHARS = 5e4;
|
|
6445
6926
|
const truncatedResponse = toolResult.success && (toolResult.truncated || toolResult.output.length > MAX_RESPONSE_CHARS);
|
|
6446
6927
|
const llmOutput = toolResult.output.length > MAX_RESPONSE_CHARS ? toolResult.output.slice(0, MAX_RESPONSE_CHARS) + `
|
|
@@ -6572,7 +7053,6 @@ ${llmOutput}
|
|
|
6572
7053
|
researchLog
|
|
6573
7054
|
};
|
|
6574
7055
|
}
|
|
6575
|
-
if (extractPrdBlock(turn.text)) ctx.prdCaptured = true;
|
|
6576
7056
|
const reply = classifyPlannerReply(turn.text, {
|
|
6577
7057
|
runners: ctx.runners,
|
|
6578
7058
|
runnerModes: ctx.runnerModes,
|
|
@@ -6581,17 +7061,11 @@ ${llmOutput}
|
|
|
6581
7061
|
switch (reply.kind) {
|
|
6582
7062
|
case "task_ops":
|
|
6583
7063
|
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
7064
|
+
// A read is settled by the Session (it owns the plan and the catalog),
|
|
7065
|
+
// so it leaves this loop the same way a plan or an edit does.
|
|
7066
|
+
case "task_query":
|
|
7067
|
+
return { kind: "task_query", query: reply.query, text: turn.text, researchLog };
|
|
6584
7068
|
case "plan":
|
|
6585
|
-
if (ctx.prdEnabled && !ctx.prdCaptured && !ctx.prdNudgeSent && !signal?.aborted) {
|
|
6586
|
-
ctx.prdNudgeSent = true;
|
|
6587
|
-
pending = [
|
|
6588
|
-
"PRD mode is enabled but no PRD has been produced yet, so the plan was NOT committed.",
|
|
6589
|
-
"In your next reply: first output the full markdown PRD wrapped EXACTLY in",
|
|
6590
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" --> and <!-- ORDEWELL_PRD_END -->,',
|
|
6591
|
-
"then re-emit the exact same task plan JSON after the PRD block."
|
|
6592
|
-
].join(" ");
|
|
6593
|
-
continue;
|
|
6594
|
-
}
|
|
6595
7069
|
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
6596
7070
|
// Botched attempts get a bounded corrective retry — otherwise the
|
|
6597
7071
|
// broken JSON would surface as a prose bubble and the edit or plan
|
|
@@ -6603,6 +7077,13 @@ ${llmOutput}
|
|
|
6603
7077
|
continue;
|
|
6604
7078
|
}
|
|
6605
7079
|
break;
|
|
7080
|
+
case "broken_task_query":
|
|
7081
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
7082
|
+
jsonRepairAttempts++;
|
|
7083
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
7084
|
+
continue;
|
|
7085
|
+
}
|
|
7086
|
+
break;
|
|
6606
7087
|
case "broken_plan":
|
|
6607
7088
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
6608
7089
|
jsonRepairAttempts++;
|
|
@@ -6626,7 +7107,7 @@ ${llmOutput}
|
|
|
6626
7107
|
* parsed tasks if a plan was emitted mid-loop, or null if the loop
|
|
6627
7108
|
* exhausted without a valid plan.
|
|
6628
7109
|
*/
|
|
6629
|
-
async runResearchLoop(chat, firstMessage,
|
|
7110
|
+
async runResearchLoop(chat, firstMessage, fs12, onProgress, runners, initialLog, fetcher, userGoal, runnerModes, autonomousDefault, signal) {
|
|
6630
7111
|
const researchLog = initialLog ? [...initialLog] : [];
|
|
6631
7112
|
if (!initialLog && userGoal) {
|
|
6632
7113
|
researchLog.push({
|
|
@@ -6653,7 +7134,7 @@ ${llmOutput}
|
|
|
6653
7134
|
if (!turn.hasToolCalls) break;
|
|
6654
7135
|
const { toolResults, logEntries, resultsText } = await this.executeToolCalls(
|
|
6655
7136
|
turn.toolCalls,
|
|
6656
|
-
|
|
7137
|
+
fs12,
|
|
6657
7138
|
onProgress,
|
|
6658
7139
|
fetcher,
|
|
6659
7140
|
step,
|
|
@@ -6789,9 +7270,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
6789
7270
|
req.runners,
|
|
6790
7271
|
req.runnerModes,
|
|
6791
7272
|
req.autonomousDefault ?? true,
|
|
6792
|
-
req.grillMeEnabled ?? false,
|
|
6793
|
-
req.prdEnabled ?? false,
|
|
6794
|
-
req.reviewEnabled ?? false,
|
|
6795
7273
|
req.verificationEnabled ?? false
|
|
6796
7274
|
);
|
|
6797
7275
|
const seededHistory = [
|
|
@@ -6830,8 +7308,7 @@ ${m.content}` });
|
|
|
6830
7308
|
runners: req.runners,
|
|
6831
7309
|
runnerModes: req.runnerModes,
|
|
6832
7310
|
autonomousDefault: req.autonomousDefault,
|
|
6833
|
-
fetcher: req.fetcher
|
|
6834
|
-
prdEnabled: req.prdEnabled ?? false
|
|
7311
|
+
fetcher: req.fetcher
|
|
6835
7312
|
};
|
|
6836
7313
|
this.conversation = { ctx, setProgress: (onProgress) => {
|
|
6837
7314
|
currentProgress = onProgress;
|
|
@@ -6889,11 +7366,11 @@ ${repairHint}` }] : parts;
|
|
|
6889
7366
|
return this.consumePlanStream(result, onToken);
|
|
6890
7367
|
}
|
|
6891
7368
|
// --- IAiService implementation ---
|
|
6892
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
7369
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
6893
7370
|
this.ensureInit();
|
|
6894
7371
|
const { autonomousDefault } = modes;
|
|
6895
7372
|
const genModel = this.getPlanningModel();
|
|
6896
|
-
const contextStr = await BaseAiService.collectResearchContext(
|
|
7373
|
+
const contextStr = await BaseAiService.collectResearchContext(fs12, runners);
|
|
6897
7374
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
6898
7375
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
6899
7376
|
|
|
@@ -6904,17 +7381,17 @@ Explore the workspace to understand the codebase, then generate the plan.`;
|
|
|
6904
7381
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
6905
7382
|
});
|
|
6906
7383
|
const researchChat = new GeminiResearchChat(chat);
|
|
6907
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
7384
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs12, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
6908
7385
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
6909
7386
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
6910
7387
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
6911
7388
|
}
|
|
6912
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
7389
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
6913
7390
|
this.ensureInit();
|
|
6914
7391
|
const { autonomousDefault } = modes;
|
|
6915
7392
|
let ctx = "";
|
|
6916
|
-
if (
|
|
6917
|
-
const collected = await new ContextCollector(
|
|
7393
|
+
if (fs12) {
|
|
7394
|
+
const collected = await new ContextCollector(fs12).collect(runners[0] ?? "claude-code");
|
|
6918
7395
|
if (collected.aiflowContext) ctx = `
|
|
6919
7396
|
<aiflow_context>
|
|
6920
7397
|
${collected.aiflowContext}
|
|
@@ -6930,12 +7407,12 @@ ${collected.aiflowContext}
|
|
|
6930
7407
|
autonomousDefault
|
|
6931
7408
|
);
|
|
6932
7409
|
}
|
|
6933
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
7410
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, _signal) {
|
|
6934
7411
|
this.ensureInit();
|
|
6935
7412
|
const { autonomousDefault } = modes;
|
|
6936
7413
|
let aiflowContext;
|
|
6937
|
-
if (
|
|
6938
|
-
const collected = await new ContextCollector(
|
|
7414
|
+
if (fs12) {
|
|
7415
|
+
const collected = await new ContextCollector(fs12).collect(existingPlan.runners[0] ?? "claude-code");
|
|
6939
7416
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
6940
7417
|
}
|
|
6941
7418
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -7118,7 +7595,6 @@ var OpenAiService = class extends BaseAiService {
|
|
|
7118
7595
|
async startConversation(req) {
|
|
7119
7596
|
this.ensureInit();
|
|
7120
7597
|
const client = this.getClient();
|
|
7121
|
-
this.researchSubagentsEnabled = req.researchSubagentsEnabled ?? false;
|
|
7122
7598
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
7123
7599
|
const systemPrompt = buildConversationSystemPrompt(
|
|
7124
7600
|
req.goal,
|
|
@@ -7127,16 +7603,13 @@ var OpenAiService = class extends BaseAiService {
|
|
|
7127
7603
|
req.runners,
|
|
7128
7604
|
req.runnerModes,
|
|
7129
7605
|
req.autonomousDefault ?? true,
|
|
7130
|
-
req.grillMeEnabled ?? false,
|
|
7131
|
-
req.prdEnabled ?? false,
|
|
7132
|
-
req.reviewEnabled ?? false,
|
|
7133
7606
|
req.verificationEnabled ?? false
|
|
7134
7607
|
);
|
|
7135
7608
|
const userText = req.goal;
|
|
7136
7609
|
const messages = [
|
|
7137
7610
|
{ role: "system", content: `${systemPrompt}
|
|
7138
7611
|
|
|
7139
|
-
${buildResearchToolsPrompt(
|
|
7612
|
+
${buildResearchToolsPrompt()}` }
|
|
7140
7613
|
];
|
|
7141
7614
|
for (const m of req.priorHistory ?? []) {
|
|
7142
7615
|
messages.push({ role: m.role, content: m.content });
|
|
@@ -7146,7 +7619,7 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
7146
7619
|
messages,
|
|
7147
7620
|
client,
|
|
7148
7621
|
this.requireModel("orchestratorModel", this.config.orchestratorModel),
|
|
7149
|
-
toOpenAiTools(
|
|
7622
|
+
toOpenAiTools(),
|
|
7150
7623
|
(delta) => currentProgress({ type: "thinking", text: delta }),
|
|
7151
7624
|
(delta) => currentProgress({ type: "plan_token", planToken: delta })
|
|
7152
7625
|
);
|
|
@@ -7156,8 +7629,7 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
7156
7629
|
runners: req.runners,
|
|
7157
7630
|
runnerModes: req.runnerModes,
|
|
7158
7631
|
autonomousDefault: req.autonomousDefault,
|
|
7159
|
-
fetcher: req.fetcher
|
|
7160
|
-
prdEnabled: req.prdEnabled ?? false
|
|
7632
|
+
fetcher: req.fetcher
|
|
7161
7633
|
};
|
|
7162
7634
|
this.conversation = {
|
|
7163
7635
|
ctx,
|
|
@@ -7175,15 +7647,14 @@ ${buildResearchToolsPrompt(this.researchSubagentsEnabled)}` }
|
|
|
7175
7647
|
}
|
|
7176
7648
|
}
|
|
7177
7649
|
// --- IAiService implementation ---
|
|
7178
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
7650
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
7179
7651
|
this.ensureInit();
|
|
7180
7652
|
const client = this.getClient();
|
|
7181
|
-
const { autonomousDefault
|
|
7182
|
-
|
|
7183
|
-
const contextStr = await BaseAiService.collectResearchContext(fs10, runners);
|
|
7653
|
+
const { autonomousDefault } = modes;
|
|
7654
|
+
const contextStr = await BaseAiService.collectResearchContext(fs12, runners);
|
|
7184
7655
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
7185
7656
|
const userText = `User goal: ${userDescription}`;
|
|
7186
|
-
const firstMessage = `${buildResearchToolsPrompt(
|
|
7657
|
+
const firstMessage = `${buildResearchToolsPrompt()}
|
|
7187
7658
|
|
|
7188
7659
|
Explore the workspace to understand the codebase, then generate the plan.
|
|
7189
7660
|
|
|
@@ -7195,20 +7666,20 @@ ${userText}`;
|
|
|
7195
7666
|
messages,
|
|
7196
7667
|
client,
|
|
7197
7668
|
this.requireModel("orchestratorModel", this.config.orchestratorModel),
|
|
7198
|
-
toOpenAiTools(
|
|
7669
|
+
toOpenAiTools(),
|
|
7199
7670
|
(delta) => onProgress({ type: "thinking", text: delta })
|
|
7200
7671
|
);
|
|
7201
|
-
const result = await this.runResearchLoop(researchChat, firstMessage,
|
|
7672
|
+
const result = await this.runResearchLoop(researchChat, firstMessage, fs12, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
7202
7673
|
if (result.tasks) return { tasks: result.tasks, researchLog: result.researchLog, researchResults: result.researchResults };
|
|
7203
7674
|
const fallback = await this.generatePlanFallback(userDescription, contextStr, result.researchResults, modelsByRunner, runners, onProgress, result.researchLog, runnerModes, autonomousDefault, signal, modes);
|
|
7204
7675
|
return { tasks: fallback.tasks, researchLog: fallback.researchLog, researchResults: result.researchResults };
|
|
7205
7676
|
}
|
|
7206
|
-
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken,
|
|
7677
|
+
async generatePlanDirect(userDescription, runners = ["claude-code"], modelsByRunner = {}, onToken, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
7207
7678
|
this.ensureInit();
|
|
7208
7679
|
const { autonomousDefault } = modes;
|
|
7209
7680
|
let ctx = "";
|
|
7210
|
-
if (
|
|
7211
|
-
const collected = await new ContextCollector(
|
|
7681
|
+
if (fs12) {
|
|
7682
|
+
const collected = await new ContextCollector(fs12).collect(runners[0] ?? "claude-code");
|
|
7212
7683
|
if (collected.aiflowContext) ctx = `
|
|
7213
7684
|
<aiflow_context>
|
|
7214
7685
|
${collected.aiflowContext}
|
|
@@ -7224,12 +7695,12 @@ ${collected.aiflowContext}
|
|
|
7224
7695
|
autonomousDefault
|
|
7225
7696
|
);
|
|
7226
7697
|
}
|
|
7227
|
-
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress,
|
|
7698
|
+
async modifyPlan(existingPlan, userRequest, modelsByRunner, onProgress, fs12, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
7228
7699
|
this.ensureInit();
|
|
7229
7700
|
const { autonomousDefault } = modes;
|
|
7230
7701
|
let aiflowContext;
|
|
7231
|
-
if (
|
|
7232
|
-
const collected = await new ContextCollector(
|
|
7702
|
+
if (fs12) {
|
|
7703
|
+
const collected = await new ContextCollector(fs12).collect(existingPlan.runners[0] ?? "claude-code");
|
|
7233
7704
|
if (collected.aiflowContext) aiflowContext = collected.aiflowContext;
|
|
7234
7705
|
}
|
|
7235
7706
|
const prompt = buildModifyPlanPrompt(existingPlan, userRequest, modelsByRunner, aiflowContext, runnerModes, autonomousDefault);
|
|
@@ -7318,21 +7789,28 @@ function pickWindow(sorted, currentIdx, max) {
|
|
|
7318
7789
|
const window = sorted.slice(start, start + max);
|
|
7319
7790
|
return { window, omitted: sorted.length - window.length };
|
|
7320
7791
|
}
|
|
7321
|
-
function renderPlanMap(
|
|
7322
|
-
|
|
7323
|
-
|
|
7792
|
+
function renderPlanMap(planTasks, currentTaskId, opts) {
|
|
7793
|
+
const rows = flattenTasksWithParents(planTasks);
|
|
7794
|
+
if (rows.length < PLAN_MAP_MIN_TASKS) return "";
|
|
7795
|
+
const sorted = rows.map((r) => ({
|
|
7796
|
+
...r,
|
|
7797
|
+
label: taskOrderLabel(r.task, r.parent ?? void 0)
|
|
7798
|
+
})).sort((a, b) => {
|
|
7799
|
+
const parentDiff = (a.parent?.order ?? -1) - (b.parent?.order ?? -1);
|
|
7800
|
+
if (parentDiff !== 0) return parentDiff;
|
|
7801
|
+
return a.task.order - b.task.order;
|
|
7802
|
+
});
|
|
7324
7803
|
const max = opts?.maxEntries ?? DEFAULT_PLAN_MAP_MAX_ENTRIES;
|
|
7325
|
-
const currentIdx = sorted.findIndex((
|
|
7804
|
+
const currentIdx = sorted.findIndex((r) => r.task.id === currentTaskId);
|
|
7326
7805
|
const { window, omitted } = pickWindow(sorted, currentIdx, max);
|
|
7327
|
-
const currentTask = sorted.find((
|
|
7328
|
-
const
|
|
7329
|
-
|
|
7330
|
-
const
|
|
7331
|
-
const
|
|
7332
|
-
const order = String(t.order).padStart(widthOrder, " ");
|
|
7806
|
+
const currentTask = sorted.find((r) => r.task.id === currentTaskId)?.task;
|
|
7807
|
+
const lines = window.map((row) => {
|
|
7808
|
+
const isCurrent = row.task.id === currentTaskId;
|
|
7809
|
+
const tag = planMapStatus(row.task, isCurrent);
|
|
7810
|
+
const order = row.parent ? row.label : row.label.padStart(2, " ");
|
|
7333
7811
|
const arrow = isCurrent ? " \u2190 you are here" : "";
|
|
7334
|
-
const runner =
|
|
7335
|
-
return `${order}. [${tag.padEnd(7, " ")}] ${
|
|
7812
|
+
const runner = row.task.assignedRunner ? ` (${row.task.assignedRunner})` : "";
|
|
7813
|
+
return `${order}. [${tag.padEnd(7, " ")}] ${row.task.title}${runner}${arrow}`;
|
|
7336
7814
|
});
|
|
7337
7815
|
const footer = omitted > 0 ? `
|
|
7338
7816
|
(${omitted} task${omitted === 1 ? "" : "s"} omitted from this view)` : "";
|
|
@@ -7397,7 +7875,8 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
7397
7875
|
const map = renderPlanMap(allTasks, task.id, { maxEntries: opts?.planMapMaxEntries });
|
|
7398
7876
|
if (map) blocks.push(map);
|
|
7399
7877
|
}
|
|
7400
|
-
const
|
|
7878
|
+
const flat = flattenTasks(allTasks);
|
|
7879
|
+
const outputs = collectDirectDependencyOutputs(task, flat);
|
|
7401
7880
|
if (outputs.length > 0) blocks.push(renderPriorOutputs(outputs));
|
|
7402
7881
|
if (opts?.tddEnabled) {
|
|
7403
7882
|
blocks.push(renderTddInstruction());
|
|
@@ -7576,6 +8055,7 @@ function renderTerminalOutput(raw) {
|
|
|
7576
8055
|
return populated.map((r) => cells(r).join("")).join("\n");
|
|
7577
8056
|
}
|
|
7578
8057
|
var MARKER_SCAN_TAIL = 16384;
|
|
8058
|
+
var IDLE_TIMEOUT_MS = 6e4;
|
|
7579
8059
|
var VerdictEngine = class {
|
|
7580
8060
|
markerSeen = /* @__PURE__ */ new Set();
|
|
7581
8061
|
buffers = /* @__PURE__ */ new Map();
|
|
@@ -7583,6 +8063,7 @@ var VerdictEngine = class {
|
|
|
7583
8063
|
pausedSessions = /* @__PURE__ */ new Map();
|
|
7584
8064
|
listeners = [];
|
|
7585
8065
|
checkpointListeners = [];
|
|
8066
|
+
idleListeners = [];
|
|
7586
8067
|
/**
|
|
7587
8068
|
* Per-task generation counter. Incremented on every watch() and clear().
|
|
7588
8069
|
* Stale callbacks (from a prior session whose generation doesn't match
|
|
@@ -7590,12 +8071,47 @@ var VerdictEngine = class {
|
|
|
7590
8071
|
* wrong session.
|
|
7591
8072
|
*/
|
|
7592
8073
|
generations = /* @__PURE__ */ new Map();
|
|
8074
|
+
idleTimers = /* @__PURE__ */ new Map();
|
|
8075
|
+
idleSince = /* @__PURE__ */ new Map();
|
|
7593
8076
|
onVerdict(listener) {
|
|
7594
8077
|
this.listeners.push(listener);
|
|
7595
8078
|
}
|
|
7596
8079
|
onCheckpoint(listener) {
|
|
7597
8080
|
this.checkpointListeners.push(listener);
|
|
7598
8081
|
}
|
|
8082
|
+
onIdleChange(listener) {
|
|
8083
|
+
this.idleListeners.push(listener);
|
|
8084
|
+
}
|
|
8085
|
+
/** Advisory silence timestamp for a task, or null if it isn't idle. */
|
|
8086
|
+
getIdleSince(taskId) {
|
|
8087
|
+
return this.idleSince.get(taskId) ?? null;
|
|
8088
|
+
}
|
|
8089
|
+
/** Restart the silence timer on fresh output; broadcasts the null transition if it was idle. */
|
|
8090
|
+
touchIdle(taskId, gen) {
|
|
8091
|
+
const existing = this.idleTimers.get(taskId);
|
|
8092
|
+
if (existing) clearTimeout(existing);
|
|
8093
|
+
if (this.idleSince.get(taskId)) {
|
|
8094
|
+
this.idleSince.set(taskId, null);
|
|
8095
|
+
for (const l of this.idleListeners) l(taskId, null);
|
|
8096
|
+
}
|
|
8097
|
+
this.idleTimers.set(taskId, setTimeout(() => {
|
|
8098
|
+
if (this.generations.get(taskId) !== gen) return;
|
|
8099
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8100
|
+
this.idleSince.set(taskId, now);
|
|
8101
|
+
for (const l of this.idleListeners) l(taskId, now);
|
|
8102
|
+
}, IDLE_TIMEOUT_MS));
|
|
8103
|
+
}
|
|
8104
|
+
/** Tear down idle tracking for a task; broadcasts the null transition if it was idle. */
|
|
8105
|
+
clearIdle(taskId) {
|
|
8106
|
+
const timer = this.idleTimers.get(taskId);
|
|
8107
|
+
if (timer) clearTimeout(timer);
|
|
8108
|
+
this.idleTimers.delete(taskId);
|
|
8109
|
+
const wasIdle = this.idleSince.get(taskId);
|
|
8110
|
+
this.idleSince.delete(taskId);
|
|
8111
|
+
if (wasIdle) {
|
|
8112
|
+
for (const l of this.idleListeners) l(taskId, null);
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
7599
8115
|
approveCheckpoint(taskId) {
|
|
7600
8116
|
const session = this.pausedSessions.get(taskId);
|
|
7601
8117
|
if (session) {
|
|
@@ -7626,6 +8142,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7626
8142
|
this.checkpointCounts.set(task.id, 0);
|
|
7627
8143
|
session.onOutput((text) => {
|
|
7628
8144
|
if (this.generations.get(task.id) !== gen) return;
|
|
8145
|
+
this.touchIdle(task.id, gen);
|
|
7629
8146
|
if (this.markerSeen.has(task.id)) return;
|
|
7630
8147
|
const buf = (this.buffers.get(task.id) ?? "") + text;
|
|
7631
8148
|
this.buffers.set(task.id, buf);
|
|
@@ -7637,6 +8154,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7637
8154
|
this.buffers.delete(task.id);
|
|
7638
8155
|
this.checkpointCounts.delete(task.id);
|
|
7639
8156
|
this.pausedSessions.delete(task.id);
|
|
8157
|
+
this.clearIdle(task.id);
|
|
7640
8158
|
this.generations.set(task.id, gen + 1);
|
|
7641
8159
|
const verdict = this.decide(task, 0);
|
|
7642
8160
|
for (const l of this.listeners) l(task.id, verdict, output);
|
|
@@ -7661,6 +8179,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7661
8179
|
this.buffers.delete(task.id);
|
|
7662
8180
|
this.checkpointCounts.delete(task.id);
|
|
7663
8181
|
this.pausedSessions.delete(task.id);
|
|
8182
|
+
this.clearIdle(task.id);
|
|
7664
8183
|
const verdict = this.decide(task, exitCode);
|
|
7665
8184
|
for (const l of this.listeners) l(task.id, verdict, output);
|
|
7666
8185
|
});
|
|
@@ -7671,6 +8190,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7671
8190
|
this.buffers.delete(task.id);
|
|
7672
8191
|
this.checkpointCounts.delete(task.id);
|
|
7673
8192
|
this.pausedSessions.delete(task.id);
|
|
8193
|
+
this.clearIdle(task.id);
|
|
7674
8194
|
this.generations.set(task.id, (this.generations.get(task.id) ?? 0) + 1);
|
|
7675
8195
|
return {
|
|
7676
8196
|
outcome: "pass",
|
|
@@ -7692,6 +8212,7 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7692
8212
|
this.buffers.delete(task.id);
|
|
7693
8213
|
this.checkpointCounts.delete(task.id);
|
|
7694
8214
|
this.pausedSessions.delete(task.id);
|
|
8215
|
+
this.clearIdle(task.id);
|
|
7695
8216
|
this.generations.set(task.id, (this.generations.get(task.id) ?? 0) + 1);
|
|
7696
8217
|
}
|
|
7697
8218
|
/** Drop all tracking state (used on stop / loadPlan). */
|
|
@@ -7700,6 +8221,9 @@ ORDEWELL_REJECT: ${reason}
|
|
|
7700
8221
|
this.buffers.clear();
|
|
7701
8222
|
this.checkpointCounts.clear();
|
|
7702
8223
|
this.pausedSessions.clear();
|
|
8224
|
+
for (const timer of this.idleTimers.values()) clearTimeout(timer);
|
|
8225
|
+
this.idleTimers.clear();
|
|
8226
|
+
this.idleSince.clear();
|
|
7703
8227
|
this.generations.clear();
|
|
7704
8228
|
}
|
|
7705
8229
|
decide(task, exitCode) {
|
|
@@ -8105,6 +8629,7 @@ var TaskOrchestrator = class {
|
|
|
8105
8629
|
this.store.markAwaitingUser(taskId);
|
|
8106
8630
|
this.emit("onCheckpoint", { taskId, taskTitle: task.title, summary });
|
|
8107
8631
|
});
|
|
8632
|
+
this.verifier.onIdleChange(() => this.emit("onTaskChanged"));
|
|
8108
8633
|
}
|
|
8109
8634
|
config;
|
|
8110
8635
|
notifications;
|
|
@@ -8129,6 +8654,10 @@ var TaskOrchestrator = class {
|
|
|
8129
8654
|
workspaceRootFn = () => process.cwd();
|
|
8130
8655
|
observers = [];
|
|
8131
8656
|
tddEnabled = () => false;
|
|
8657
|
+
/** Advisory silence timestamp for a task's live runner, or null if not idle. */
|
|
8658
|
+
getIdleSince(taskId) {
|
|
8659
|
+
return this.verifier.getIdleSince(taskId);
|
|
8660
|
+
}
|
|
8132
8661
|
get storeInstance() {
|
|
8133
8662
|
return this.store;
|
|
8134
8663
|
}
|
|
@@ -8516,8 +9045,10 @@ ${output.slice(-3e3)}`);
|
|
|
8516
9045
|
if (ready.length === 0 && this.activeTaskSessions.size === 0) {
|
|
8517
9046
|
const remaining = this.store.allTasks.filter((t) => t.status !== "completed");
|
|
8518
9047
|
console.log(`[TaskOrchestrator] tick(): no work. remaining=${remaining.length}, completed=${this.store.isAllComplete()}`);
|
|
8519
|
-
|
|
8520
|
-
|
|
9048
|
+
const remainingById = new Map(remaining.map((t) => [t.id, t]));
|
|
9049
|
+
for (const { task, parent } of flattenTasksWithParents(this.store.planTasks)) {
|
|
9050
|
+
if (!remainingById.has(task.id)) continue;
|
|
9051
|
+
console.log(` - ${taskOrderLabel(task, parent ?? void 0)}. ${task.title} [${task.type}/${task.status}] prompt=${!!task.prompt}`);
|
|
8521
9052
|
}
|
|
8522
9053
|
if (this.store.isAllComplete()) {
|
|
8523
9054
|
this.running = false;
|
|
@@ -8544,148 +9075,43 @@ ${output.slice(-3e3)}`);
|
|
|
8544
9075
|
try {
|
|
8545
9076
|
const cwd = this.workspaceRootFn();
|
|
8546
9077
|
const runner = this.store.resolveTaskRunner(task);
|
|
8547
|
-
const finalPrompt = composeAugmentedPrompt(task, this.store.
|
|
9078
|
+
const finalPrompt = composeAugmentedPrompt(task, this.store.planTasks, {
|
|
8548
9079
|
planMapEnabled: this.config.planMapEnabled,
|
|
8549
9080
|
tddEnabled: this.tddEnabled()
|
|
8550
9081
|
});
|
|
8551
|
-
const session = await this.terminalRunner.spawn({
|
|
8552
|
-
taskId: task.id,
|
|
8553
|
-
runner,
|
|
8554
|
-
prompt: finalPrompt,
|
|
8555
|
-
modelId: task.assignedModel?.modelId,
|
|
8556
|
-
thinkingEffort: task.assignedModel?.thinkingEffort,
|
|
8557
|
-
modelVariants: task.assignedModel?.availableVariants,
|
|
8558
|
-
mode: task.taskMode ?? "build",
|
|
8559
|
-
cwd,
|
|
8560
|
-
registry: this.registry ?? void 0,
|
|
8561
|
-
order: task.order,
|
|
8562
|
-
title: task.title
|
|
8563
|
-
});
|
|
8564
|
-
if (!this.startingTaskIds.delete(task.id)) {
|
|
8565
|
-
session.kill();
|
|
8566
|
-
this.store.markPending(task.id);
|
|
8567
|
-
this.emit("onTaskChanged");
|
|
8568
|
-
return;
|
|
8569
|
-
}
|
|
8570
|
-
this.activeTaskSessions.set(task.id, session.id);
|
|
8571
|
-
this.verifier.watch(task, session);
|
|
8572
|
-
this.emit("onTaskChanged");
|
|
8573
|
-
this.notifications.info(`Task "${task.title}" started (${runner})`);
|
|
8574
|
-
} catch (err) {
|
|
8575
|
-
this.startingTaskIds.delete(task.id);
|
|
8576
|
-
this.store.markPending(task.id);
|
|
8577
|
-
this.onHold.add(task.id);
|
|
8578
|
-
this.notifications.error(`Failed to start task "${task.title}": ${err}`);
|
|
8579
|
-
this.emit("onTaskChanged");
|
|
8580
|
-
await this.tick();
|
|
8581
|
-
}
|
|
8582
|
-
}
|
|
8583
|
-
};
|
|
8584
|
-
|
|
8585
|
-
// src/services/ModelAllowlistResolver.ts
|
|
8586
|
-
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
8587
|
-
if (!allowlist || allowlist.length === 0) return void 0;
|
|
8588
|
-
if (!modelsByRunner) return allowlist;
|
|
8589
|
-
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
8590
|
-
const elsewhere = /* @__PURE__ */ new Set();
|
|
8591
|
-
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
8592
|
-
if (other === runner) continue;
|
|
8593
|
-
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
8594
|
-
}
|
|
8595
|
-
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
8596
|
-
return kept.length > 0 ? kept : void 0;
|
|
8597
|
-
}
|
|
8598
|
-
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
8599
|
-
const result = {};
|
|
8600
|
-
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
8601
|
-
if (!models) continue;
|
|
8602
|
-
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
8603
|
-
if (!allowed) {
|
|
8604
|
-
result[runner] = models;
|
|
8605
|
-
continue;
|
|
8606
|
-
}
|
|
8607
|
-
const allowedSet = new Set(allowed);
|
|
8608
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
8609
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
8610
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
8611
|
-
result[runner] = [...intersection, ...missing];
|
|
8612
|
-
}
|
|
8613
|
-
return result;
|
|
8614
|
-
}
|
|
8615
|
-
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
8616
|
-
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
8617
|
-
function clampThinkingEffort(effort, variants) {
|
|
8618
|
-
if (!effort) return void 0;
|
|
8619
|
-
if (variants.length === 0) return void 0;
|
|
8620
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
8621
|
-
if (ids.has(effort)) return effort;
|
|
8622
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
8623
|
-
if (ids.has(canon)) return canon;
|
|
8624
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
8625
|
-
if (idx < 0) return void 0;
|
|
8626
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
8627
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
8628
|
-
if (lower && ids.has(lower)) return lower;
|
|
8629
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
8630
|
-
if (higher && ids.has(higher)) return higher;
|
|
8631
|
-
}
|
|
8632
|
-
return void 0;
|
|
8633
|
-
}
|
|
8634
|
-
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
8635
|
-
const clampVariant = (task) => {
|
|
8636
|
-
if (!task.assignedModel || !modelsByRunner) return task;
|
|
8637
|
-
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
8638
|
-
(m) => m.modelId === task.assignedModel.modelId
|
|
8639
|
-
);
|
|
8640
|
-
if (!model) return task;
|
|
8641
|
-
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
8642
|
-
return {
|
|
8643
|
-
...task,
|
|
8644
|
-
assignedModel: {
|
|
8645
|
-
...task.assignedModel,
|
|
8646
|
-
thinkingEffort: clamped,
|
|
8647
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
8648
|
-
}
|
|
8649
|
-
};
|
|
8650
|
-
};
|
|
8651
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
8652
|
-
const assignmentFor = (runner, modelId) => {
|
|
8653
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
8654
|
-
return {
|
|
8655
|
-
modelId,
|
|
8656
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
8657
|
-
thinkingEffort: void 0,
|
|
8658
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
8659
|
-
};
|
|
8660
|
-
};
|
|
8661
|
-
return tasks.map((task) => {
|
|
8662
|
-
if (task.type === "user") return task;
|
|
8663
|
-
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
8664
|
-
const fallbackRunner = allowedRunners[0];
|
|
8665
|
-
const fallbackModels = allowedFor(fallbackRunner);
|
|
8666
|
-
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
8667
|
-
return clampVariant({
|
|
8668
|
-
...task,
|
|
8669
|
-
assignedRunner: fallbackRunner,
|
|
8670
|
-
assignedModel: fallbackModel
|
|
9082
|
+
const session = await this.terminalRunner.spawn({
|
|
9083
|
+
taskId: task.id,
|
|
9084
|
+
runner,
|
|
9085
|
+
prompt: finalPrompt,
|
|
9086
|
+
modelId: task.assignedModel?.modelId,
|
|
9087
|
+
thinkingEffort: task.assignedModel?.thinkingEffort,
|
|
9088
|
+
modelVariants: task.assignedModel?.availableVariants,
|
|
9089
|
+
mode: task.taskMode ?? "build",
|
|
9090
|
+
cwd,
|
|
9091
|
+
registry: this.registry ?? void 0,
|
|
9092
|
+
order: task.order,
|
|
9093
|
+
title: task.title
|
|
8671
9094
|
});
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
return
|
|
8677
|
-
}
|
|
8678
|
-
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
8679
|
-
if (discovered) {
|
|
8680
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
9095
|
+
if (!this.startingTaskIds.delete(task.id)) {
|
|
9096
|
+
session.kill();
|
|
9097
|
+
this.store.markPending(task.id);
|
|
9098
|
+
this.emit("onTaskChanged");
|
|
9099
|
+
return;
|
|
8681
9100
|
}
|
|
8682
|
-
|
|
9101
|
+
this.activeTaskSessions.set(task.id, session.id);
|
|
9102
|
+
this.verifier.watch(task, session);
|
|
9103
|
+
this.emit("onTaskChanged");
|
|
9104
|
+
this.notifications.info(`Task "${task.title}" started (${runner})`);
|
|
9105
|
+
} catch (err) {
|
|
9106
|
+
this.startingTaskIds.delete(task.id);
|
|
9107
|
+
this.store.markPending(task.id);
|
|
9108
|
+
this.onHold.add(task.id);
|
|
9109
|
+
this.notifications.error(`Failed to start task "${task.title}": ${err}`);
|
|
9110
|
+
this.emit("onTaskChanged");
|
|
9111
|
+
await this.tick();
|
|
8683
9112
|
}
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
8687
|
-
});
|
|
8688
|
-
}
|
|
9113
|
+
}
|
|
9114
|
+
};
|
|
8689
9115
|
|
|
8690
9116
|
// src/services/Planner.ts
|
|
8691
9117
|
function plannerModesWith(req) {
|
|
@@ -8823,7 +9249,7 @@ var osMod = __toESM(require("os"));
|
|
|
8823
9249
|
var import_child_process = require("child_process");
|
|
8824
9250
|
var import_util = require("util");
|
|
8825
9251
|
var os2 = __toESM(require("os"));
|
|
8826
|
-
var
|
|
9252
|
+
var path4 = __toESM(require("path"));
|
|
8827
9253
|
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
8828
9254
|
var LOGIN_SHELL_TIMEOUT_MS = 5e3;
|
|
8829
9255
|
var PATH_MARKER = "__ORDEWELL_PATH__";
|
|
@@ -8834,40 +9260,40 @@ var defaultExec = async (command, options) => {
|
|
|
8834
9260
|
function posixBinDirs() {
|
|
8835
9261
|
const home = os2.homedir();
|
|
8836
9262
|
return [
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
9263
|
+
path4.join(home, ".local", "bin"),
|
|
9264
|
+
path4.join(home, ".opencode", "bin"),
|
|
9265
|
+
path4.join(home, ".claude", "bin"),
|
|
9266
|
+
path4.join(home, ".bun", "bin"),
|
|
9267
|
+
path4.join(home, ".volta", "bin"),
|
|
9268
|
+
path4.join(home, ".npm-global", "bin"),
|
|
9269
|
+
path4.join(home, "bin"),
|
|
8844
9270
|
"/usr/local/bin",
|
|
8845
9271
|
"/opt/homebrew/bin"
|
|
8846
9272
|
];
|
|
8847
9273
|
}
|
|
8848
9274
|
function windowsBinDirs(env, home) {
|
|
8849
|
-
const
|
|
8850
|
-
const under = (base, ...parts) => base ? [
|
|
9275
|
+
const join19 = (...parts) => path4.win32.join(...parts);
|
|
9276
|
+
const under = (base, ...parts) => base ? [join19(base, ...parts)] : [];
|
|
8851
9277
|
const appData = env.APPDATA;
|
|
8852
9278
|
const localAppData = env.LOCALAPPDATA;
|
|
8853
9279
|
const programData = env.ProgramData;
|
|
8854
9280
|
return [
|
|
8855
9281
|
// Native per-user installers, PowerShell-driven and otherwise.
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
9282
|
+
join19(home, ".local", "bin"),
|
|
9283
|
+
join19(home, ".opencode", "bin"),
|
|
9284
|
+
join19(home, ".claude", "bin"),
|
|
9285
|
+
join19(home, ".codex", "bin"),
|
|
8860
9286
|
...under(localAppData, "Programs", "claude"),
|
|
8861
9287
|
// Node package managers. npm's global prefix is `%APPDATA%\npm`; the
|
|
8862
9288
|
// literal spelling is kept as a fallback for a host that does not export
|
|
8863
9289
|
// APPDATA (a service account, a stripped CI container).
|
|
8864
9290
|
...under(appData, "npm"),
|
|
8865
|
-
|
|
9291
|
+
join19(home, "AppData", "Roaming", "npm"),
|
|
8866
9292
|
...under(localAppData, "pnpm"),
|
|
8867
9293
|
...under(localAppData, "Yarn", "bin"),
|
|
8868
|
-
|
|
9294
|
+
join19(home, ".bun", "bin"),
|
|
8869
9295
|
// Windows package managers.
|
|
8870
|
-
|
|
9296
|
+
join19(home, "scoop", "shims"),
|
|
8871
9297
|
...under(env.SCOOP, "shims"),
|
|
8872
9298
|
...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
|
|
8873
9299
|
...under(programData, "chocolatey", "bin"),
|
|
@@ -8877,7 +9303,7 @@ function windowsBinDirs(env, home) {
|
|
|
8877
9303
|
...under(localAppData, "Microsoft", "WindowsApps"),
|
|
8878
9304
|
// Version managers. Volta's Windows home is not the POSIX `~/.volta`.
|
|
8879
9305
|
...under(localAppData, "Volta", "bin"),
|
|
8880
|
-
|
|
9306
|
+
join19(home, ".volta", "bin")
|
|
8881
9307
|
];
|
|
8882
9308
|
}
|
|
8883
9309
|
function wellKnownBinDirs(deps = {}) {
|
|
@@ -8906,14 +9332,14 @@ function augmentedPath(execImpl = defaultExec) {
|
|
|
8906
9332
|
const segments = [];
|
|
8907
9333
|
const push = (p) => {
|
|
8908
9334
|
if (!p) return;
|
|
8909
|
-
for (const seg of p.split(
|
|
9335
|
+
for (const seg of p.split(path4.delimiter)) {
|
|
8910
9336
|
if (seg && !segments.includes(seg)) segments.push(seg);
|
|
8911
9337
|
}
|
|
8912
9338
|
};
|
|
8913
9339
|
push(process.env.PATH);
|
|
8914
9340
|
push(await loginShellPath(execImpl));
|
|
8915
9341
|
for (const dir of wellKnownBinDirs()) push(dir);
|
|
8916
|
-
return segments.join(
|
|
9342
|
+
return segments.join(path4.delimiter);
|
|
8917
9343
|
})();
|
|
8918
9344
|
}
|
|
8919
9345
|
return cachedPath;
|
|
@@ -8933,6 +9359,50 @@ function withPath(base, resolvedPath, overrides = {}) {
|
|
|
8933
9359
|
return env;
|
|
8934
9360
|
}
|
|
8935
9361
|
|
|
9362
|
+
// src/utils/globalDataDir.ts
|
|
9363
|
+
var fs = __toESM(require("fs"));
|
|
9364
|
+
var path5 = __toESM(require("path"));
|
|
9365
|
+
var os3 = __toESM(require("os"));
|
|
9366
|
+
function globalDataDir() {
|
|
9367
|
+
return path5.join(os3.homedir(), ".ordewell");
|
|
9368
|
+
}
|
|
9369
|
+
function copyDirSync(src, dest) {
|
|
9370
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
9371
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
9372
|
+
for (const entry of entries) {
|
|
9373
|
+
const srcPath = path5.join(src, entry.name);
|
|
9374
|
+
const destPath = path5.join(dest, entry.name);
|
|
9375
|
+
if (entry.isDirectory()) {
|
|
9376
|
+
copyDirSync(srcPath, destPath);
|
|
9377
|
+
} else {
|
|
9378
|
+
fs.copyFileSync(srcPath, destPath);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
9382
|
+
var migrated = false;
|
|
9383
|
+
function migrateOldConfigDir() {
|
|
9384
|
+
if (migrated) return;
|
|
9385
|
+
migrated = true;
|
|
9386
|
+
const oldDir = path5.join(os3.homedir(), ".config", "ordewell");
|
|
9387
|
+
if (!fs.existsSync(oldDir)) return;
|
|
9388
|
+
const newDir = globalDataDir();
|
|
9389
|
+
const liftFile = (name) => {
|
|
9390
|
+
const oldFile = path5.join(oldDir, name);
|
|
9391
|
+
const newFile = path5.join(newDir, name);
|
|
9392
|
+
if (fs.existsSync(oldFile) && !fs.existsSync(newFile)) {
|
|
9393
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
9394
|
+
fs.copyFileSync(oldFile, newFile);
|
|
9395
|
+
}
|
|
9396
|
+
};
|
|
9397
|
+
liftFile("settings.json");
|
|
9398
|
+
liftFile(".env");
|
|
9399
|
+
const oldPlugins = path5.join(oldDir, "plugins");
|
|
9400
|
+
const newPlugins = path5.join(newDir, "plugins");
|
|
9401
|
+
if (fs.existsSync(oldPlugins) && !fs.existsSync(newPlugins)) {
|
|
9402
|
+
copyDirSync(oldPlugins, newPlugins);
|
|
9403
|
+
}
|
|
9404
|
+
}
|
|
9405
|
+
|
|
8936
9406
|
// src/utils/launch.ts
|
|
8937
9407
|
var fsSync2 = __toESM(require("fs"));
|
|
8938
9408
|
var path6 = __toESM(require("path"));
|
|
@@ -9258,7 +9728,7 @@ function parseCodexModelsCache(content) {
|
|
|
9258
9728
|
}
|
|
9259
9729
|
var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9260
9730
|
function geminiDiscoveryCacheKey(apiKey) {
|
|
9261
|
-
const dir =
|
|
9731
|
+
const dir = globalDataDir();
|
|
9262
9732
|
const hash = apiKey.slice(-8);
|
|
9263
9733
|
return {
|
|
9264
9734
|
key: `gemini-models:${hash}`,
|
|
@@ -9788,12 +10258,14 @@ var ModelDiscovery = class {
|
|
|
9788
10258
|
// src/services/ModelCatalog.ts
|
|
9789
10259
|
var fs2 = __toESM(require("fs"));
|
|
9790
10260
|
var path8 = __toESM(require("path"));
|
|
9791
|
-
var os3 = __toESM(require("os"));
|
|
9792
10261
|
var CACHE_KEY = "model-catalog";
|
|
9793
10262
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
9794
10263
|
function storageKey(baseUrl) {
|
|
9795
10264
|
return `${CACHE_KEY}:${baseUrl}`;
|
|
9796
10265
|
}
|
|
10266
|
+
function globalCacheDir() {
|
|
10267
|
+
return globalDataDir();
|
|
10268
|
+
}
|
|
9797
10269
|
var ModelCatalog = class {
|
|
9798
10270
|
static async fetchModels(apiKey, baseUrl, fetchImpl) {
|
|
9799
10271
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
@@ -9836,7 +10308,7 @@ var ModelCatalog = class {
|
|
|
9836
10308
|
try {
|
|
9837
10309
|
const url = (baseUrl || "https://openrouter.ai/api/v1").replace(/\/+$/, "");
|
|
9838
10310
|
const key = storageKey(url);
|
|
9839
|
-
const cacheDir =
|
|
10311
|
+
const cacheDir = globalCacheDir();
|
|
9840
10312
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
9841
10313
|
if (fs2.existsSync(cacheFile)) fs2.rmSync(cacheFile);
|
|
9842
10314
|
} catch {
|
|
@@ -9844,7 +10316,7 @@ var ModelCatalog = class {
|
|
|
9844
10316
|
}
|
|
9845
10317
|
static readCache(key) {
|
|
9846
10318
|
try {
|
|
9847
|
-
const cacheDir =
|
|
10319
|
+
const cacheDir = globalCacheDir();
|
|
9848
10320
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
9849
10321
|
if (!fs2.existsSync(cacheFile)) return null;
|
|
9850
10322
|
const raw = fs2.readFileSync(cacheFile, "utf8");
|
|
@@ -9857,7 +10329,7 @@ var ModelCatalog = class {
|
|
|
9857
10329
|
}
|
|
9858
10330
|
static writeCache(key, models) {
|
|
9859
10331
|
try {
|
|
9860
|
-
const cacheDir =
|
|
10332
|
+
const cacheDir = globalCacheDir();
|
|
9861
10333
|
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
9862
10334
|
const cacheFile = path8.join(cacheDir, `${key.replace(/[:/]/g, "_")}.json`);
|
|
9863
10335
|
fs2.writeFileSync(
|
|
@@ -10398,12 +10870,19 @@ var StdioAgentAdapter = class {
|
|
|
10398
10870
|
/** Set for the duration of a turn. Outside one, events are buffered rather than dropped. */
|
|
10399
10871
|
turnEmit = null;
|
|
10400
10872
|
/**
|
|
10401
|
-
* Events the agent produced
|
|
10402
|
-
* that arrive during the handshake. Dropping them hid the one
|
|
10403
|
-
* that explains a planner which cannot read the workspace, so they
|
|
10404
|
-
* until a turn exists to show them in.
|
|
10873
|
+
* Events the agent produced before its first turn — in practice the startup
|
|
10874
|
+
* warnings that arrive during the handshake. Dropping them hid the one
|
|
10875
|
+
* diagnostic that explains a planner which cannot read the workspace, so they
|
|
10876
|
+
* are held until a turn exists to show them in.
|
|
10877
|
+
*
|
|
10878
|
+
* Only before the *first* turn. Once one has run, anything arriving outside a
|
|
10879
|
+
* turn is that closed turn's straggling work — a backgrounded subagent
|
|
10880
|
+
* finishing, a late read — and replaying it into the next turn presents it as
|
|
10881
|
+
* research done for the user's new message.
|
|
10405
10882
|
*/
|
|
10406
10883
|
betweenTurns = [];
|
|
10884
|
+
/** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
|
|
10885
|
+
hadTurn = false;
|
|
10407
10886
|
disposed = false;
|
|
10408
10887
|
/** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
|
|
10409
10888
|
processEnded;
|
|
@@ -10438,7 +10917,7 @@ var StdioAgentAdapter = class {
|
|
|
10438
10917
|
this.process.stdout?.on("data", (chunk) => {
|
|
10439
10918
|
this.stdout.push(chunk.toString(), (line) => this.handleLine(line, (event) => {
|
|
10440
10919
|
if (this.turnEmit) this.turnEmit(event);
|
|
10441
|
-
else if (!TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
10920
|
+
else if (!this.hadTurn && !TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
10442
10921
|
this.betweenTurns.push(event);
|
|
10443
10922
|
}
|
|
10444
10923
|
}));
|
|
@@ -10470,6 +10949,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
10470
10949
|
if (settled) return;
|
|
10471
10950
|
settled = true;
|
|
10472
10951
|
this.turnEmit = null;
|
|
10952
|
+
this.hadTurn = true;
|
|
10473
10953
|
signal?.removeEventListener("abort", onAbort);
|
|
10474
10954
|
this.process?.removeListener("exit", onExit);
|
|
10475
10955
|
this.process?.removeListener("error", onExit);
|
|
@@ -10548,6 +11028,7 @@ ${tail}` : ""}`;
|
|
|
10548
11028
|
|
|
10549
11029
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
10550
11030
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
11031
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
10551
11032
|
function flattenContent(content) {
|
|
10552
11033
|
if (typeof content === "string") return content;
|
|
10553
11034
|
if (Array.isArray(content)) {
|
|
@@ -10557,6 +11038,8 @@ function flattenContent(content) {
|
|
|
10557
11038
|
}
|
|
10558
11039
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
10559
11040
|
agentId = "claude-code";
|
|
11041
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
11042
|
+
turnHasText = false;
|
|
10560
11043
|
spawnSpec(opts) {
|
|
10561
11044
|
const args = [
|
|
10562
11045
|
"-p",
|
|
@@ -10581,6 +11064,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10581
11064
|
return { command: "claude", args };
|
|
10582
11065
|
}
|
|
10583
11066
|
turnPayload(message) {
|
|
11067
|
+
this.turnHasText = false;
|
|
10584
11068
|
return `${JSON.stringify({
|
|
10585
11069
|
type: "user",
|
|
10586
11070
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
@@ -10591,6 +11075,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10591
11075
|
const msg = StdioAgentAdapter.parse(line);
|
|
10592
11076
|
if (!msg) return;
|
|
10593
11077
|
if (msg.session_id) this.sessionId = msg.session_id;
|
|
11078
|
+
if (msg.parent_tool_use_id) return;
|
|
10594
11079
|
switch (msg.type) {
|
|
10595
11080
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
10596
11081
|
// cannot decide alone. A read-only planner answers "deny", every time —
|
|
@@ -10615,8 +11100,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10615
11100
|
}
|
|
10616
11101
|
case "assistant":
|
|
10617
11102
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
10618
|
-
if (block.type === "text" && block.text)
|
|
10619
|
-
|
|
11103
|
+
if (block.type === "text" && block.text) {
|
|
11104
|
+
emit({ type: "assistant_text", text: this.turnHasText ? `
|
|
11105
|
+
|
|
11106
|
+
${block.text}` : block.text });
|
|
11107
|
+
this.turnHasText = true;
|
|
11108
|
+
} else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
|
|
10620
11109
|
else if (block.type === "tool_use" && block.name) {
|
|
10621
11110
|
emit({ type: "tool_call", id: block.id ?? block.name, name: block.name, args: block.input ?? {} });
|
|
10622
11111
|
}
|
|
@@ -10625,11 +11114,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10625
11114
|
case "user":
|
|
10626
11115
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
10627
11116
|
if (block.type !== "tool_result") continue;
|
|
11117
|
+
const output = flattenContent(block.content);
|
|
11118
|
+
if (output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
11119
|
+
emit({ type: "background_agent", id: block.tool_use_id ?? "" });
|
|
11120
|
+
}
|
|
10628
11121
|
emit({
|
|
10629
11122
|
type: "tool_result",
|
|
10630
11123
|
id: block.tool_use_id ?? "",
|
|
10631
11124
|
name: "",
|
|
10632
|
-
output
|
|
11125
|
+
output,
|
|
10633
11126
|
success: block.is_error !== true
|
|
10634
11127
|
});
|
|
10635
11128
|
}
|
|
@@ -11049,6 +11542,8 @@ var OpenCodeAdapter = class {
|
|
|
11049
11542
|
exited = false;
|
|
11050
11543
|
disposed = false;
|
|
11051
11544
|
opts = null;
|
|
11545
|
+
/** Whether this turn has already emitted reply text — see {@link emitPart}. */
|
|
11546
|
+
turnHasText = false;
|
|
11052
11547
|
async start(opts) {
|
|
11053
11548
|
this.opts = opts;
|
|
11054
11549
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
@@ -11116,6 +11611,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11116
11611
|
return;
|
|
11117
11612
|
}
|
|
11118
11613
|
const seen = /* @__PURE__ */ new Set();
|
|
11614
|
+
this.turnHasText = false;
|
|
11119
11615
|
const streamAbort = new AbortController();
|
|
11120
11616
|
let connected = () => {
|
|
11121
11617
|
};
|
|
@@ -11184,7 +11680,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11184
11680
|
if (part.type === "text" && part.text) {
|
|
11185
11681
|
if (seen.has(`text:${id}`)) return;
|
|
11186
11682
|
seen.add(`text:${id}`);
|
|
11187
|
-
onEvent({ type: "assistant_text", text:
|
|
11683
|
+
onEvent({ type: "assistant_text", text: this.turnHasText ? `
|
|
11684
|
+
|
|
11685
|
+
${part.text}` : part.text });
|
|
11686
|
+
this.turnHasText = true;
|
|
11188
11687
|
return;
|
|
11189
11688
|
}
|
|
11190
11689
|
if (part.type === "reasoning" && part.text) {
|
|
@@ -11272,14 +11771,14 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
11272
11771
|
}
|
|
11273
11772
|
}
|
|
11274
11773
|
}
|
|
11275
|
-
async json(method,
|
|
11276
|
-
const response = await this.deps.fetch(`${this.baseUrl}${
|
|
11774
|
+
async json(method, path20, body, signal) {
|
|
11775
|
+
const response = await this.deps.fetch(`${this.baseUrl}${path20}`, {
|
|
11277
11776
|
method,
|
|
11278
11777
|
headers: { "content-type": "application/json" },
|
|
11279
11778
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
11280
11779
|
...signal ? { signal } : {}
|
|
11281
11780
|
});
|
|
11282
|
-
if (!response.ok) throw new Error(`${method} ${
|
|
11781
|
+
if (!response.ok) throw new Error(`${method} ${path20} failed: ${response.status} ${response.statusText}`);
|
|
11283
11782
|
return await response.json().catch(() => null);
|
|
11284
11783
|
}
|
|
11285
11784
|
nativeSessionId() {
|
|
@@ -11351,6 +11850,7 @@ function normalizeAgentArgs(tool, args) {
|
|
|
11351
11850
|
|
|
11352
11851
|
// src/services/harness/CliAgentAiService.ts
|
|
11353
11852
|
var MAX_JSON_REPAIRS = 2;
|
|
11853
|
+
var MAX_AGENT_WAITS = 2;
|
|
11354
11854
|
var LOG_MAX_CHARS = 1e4;
|
|
11355
11855
|
function defaultAdapter(runner, deps) {
|
|
11356
11856
|
switch (runner) {
|
|
@@ -11433,9 +11933,6 @@ var CliAgentAiService = class {
|
|
|
11433
11933
|
req.runners,
|
|
11434
11934
|
req.runnerModes,
|
|
11435
11935
|
req.autonomousDefault ?? true,
|
|
11436
|
-
req.grillMeEnabled ?? false,
|
|
11437
|
-
req.prdEnabled ?? false,
|
|
11438
|
-
req.reviewEnabled ?? false,
|
|
11439
11936
|
req.verificationEnabled ?? false,
|
|
11440
11937
|
true
|
|
11441
11938
|
);
|
|
@@ -11450,10 +11947,7 @@ var CliAgentAiService = class {
|
|
|
11450
11947
|
startOptions,
|
|
11451
11948
|
runners: req.runners,
|
|
11452
11949
|
runnerModes: req.runnerModes,
|
|
11453
|
-
autonomousDefault: req.autonomousDefault
|
|
11454
|
-
prdEnabled: req.prdEnabled ?? false,
|
|
11455
|
-
prdCaptured: false,
|
|
11456
|
-
prdNudgeSent: false
|
|
11950
|
+
autonomousDefault: req.autonomousDefault
|
|
11457
11951
|
};
|
|
11458
11952
|
const opening = this.openingMessage(req);
|
|
11459
11953
|
return this.runConversation(opening, req.onProgress, req.signal);
|
|
@@ -11481,8 +11975,8 @@ var CliAgentAiService = class {
|
|
|
11481
11975
|
/**
|
|
11482
11976
|
* Drive one user message to a settled planner turn. Same shape as the API
|
|
11483
11977
|
* backend's conversation loop minus the tool rounds — those belong to the
|
|
11484
|
-
* agent now — and with the same
|
|
11485
|
-
*
|
|
11978
|
+
* agent now — and with the same two policies layered on top: the
|
|
11979
|
+
* empty-reply nudge, and a bounded corrective re-emit for botched JSON.
|
|
11486
11980
|
*/
|
|
11487
11981
|
async runConversation(message, onProgress, signal) {
|
|
11488
11982
|
const conversation = this.conversation;
|
|
@@ -11491,13 +11985,16 @@ var CliAgentAiService = class {
|
|
|
11491
11985
|
let pending = message;
|
|
11492
11986
|
let emptyNudgeSent = false;
|
|
11493
11987
|
let jsonRepairAttempts = 0;
|
|
11988
|
+
let agentWaits = 0;
|
|
11989
|
+
const carried = [];
|
|
11990
|
+
const replyText = (text) => [...carried, text].filter((part) => part.trim()).join("\n\n");
|
|
11494
11991
|
try {
|
|
11495
11992
|
for (; ; ) {
|
|
11496
11993
|
const turn = await this.runTurn(pending, onProgress, combined);
|
|
11497
11994
|
researchLog.push(...turn.researchLog);
|
|
11498
11995
|
if (turn.aborted || combined?.aborted) {
|
|
11499
11996
|
onProgress({ type: "interrupted" });
|
|
11500
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
11997
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
11501
11998
|
}
|
|
11502
11999
|
if (turn.error) {
|
|
11503
12000
|
return { kind: "message", text: turn.error, researchLog };
|
|
@@ -11515,7 +12012,18 @@ var CliAgentAiService = class {
|
|
|
11515
12012
|
researchLog
|
|
11516
12013
|
};
|
|
11517
12014
|
}
|
|
11518
|
-
if (
|
|
12015
|
+
if (turn.backgroundAgents > 0 && agentWaits < MAX_AGENT_WAITS && !combined?.aborted) {
|
|
12016
|
+
agentWaits++;
|
|
12017
|
+
carried.push(turn.text);
|
|
12018
|
+
pending = [
|
|
12019
|
+
`You ended your turn with ${turn.backgroundAgents} subagent(s) still running in the background.`,
|
|
12020
|
+
"Ordewell hands the conversation back to the user when your turn ends, so anything you say after it never reaches them \u2014",
|
|
12021
|
+
"the results you promised to report would be lost.",
|
|
12022
|
+
"Wait for those agents to finish NOW, in this reply, and do not end your turn until you have their results.",
|
|
12023
|
+
"Then give the user your synthesis. In future replies, await your agents inside the turn rather than backgrounding them."
|
|
12024
|
+
].join(" ");
|
|
12025
|
+
continue;
|
|
12026
|
+
}
|
|
11519
12027
|
const reply = classifyPlannerReply(turn.text, {
|
|
11520
12028
|
runners: conversation.runners,
|
|
11521
12029
|
runnerModes: conversation.runnerModes,
|
|
@@ -11523,20 +12031,14 @@ var CliAgentAiService = class {
|
|
|
11523
12031
|
});
|
|
11524
12032
|
switch (reply.kind) {
|
|
11525
12033
|
case "task_ops":
|
|
11526
|
-
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
12034
|
+
return { kind: "task_ops", ops: reply.ops, text: replyText(turn.text), researchLog };
|
|
12035
|
+
// The read channel is a text envelope precisely so it reaches here
|
|
12036
|
+
// too: a harness planner has no Ordewell tool loop to call into.
|
|
12037
|
+
case "task_query":
|
|
12038
|
+
return { kind: "task_query", query: reply.query, text: replyText(turn.text), researchLog };
|
|
11527
12039
|
case "plan":
|
|
11528
|
-
if (conversation.prdEnabled && !conversation.prdCaptured && !conversation.prdNudgeSent && !combined?.aborted) {
|
|
11529
|
-
conversation.prdNudgeSent = true;
|
|
11530
|
-
pending = [
|
|
11531
|
-
"PRD mode is enabled but no PRD has been produced yet, so the plan was NOT committed.",
|
|
11532
|
-
"In your next reply: first output the full markdown PRD wrapped EXACTLY in",
|
|
11533
|
-
'<!-- ORDEWELL_PRD_START slug="<feature-slug>" --> and <!-- ORDEWELL_PRD_END -->,',
|
|
11534
|
-
"then re-emit the exact same task plan JSON after the PRD block."
|
|
11535
|
-
].join(" ");
|
|
11536
|
-
continue;
|
|
11537
|
-
}
|
|
11538
12040
|
this.conversation = null;
|
|
11539
|
-
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
12041
|
+
return { kind: "plan", tasks: reply.tasks, text: replyText(turn.text), researchLog };
|
|
11540
12042
|
case "broken_task_ops":
|
|
11541
12043
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
11542
12044
|
jsonRepairAttempts++;
|
|
@@ -11544,6 +12046,13 @@ var CliAgentAiService = class {
|
|
|
11544
12046
|
continue;
|
|
11545
12047
|
}
|
|
11546
12048
|
break;
|
|
12049
|
+
case "broken_task_query":
|
|
12050
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
12051
|
+
jsonRepairAttempts++;
|
|
12052
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
12053
|
+
continue;
|
|
12054
|
+
}
|
|
12055
|
+
break;
|
|
11547
12056
|
case "broken_plan":
|
|
11548
12057
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
11549
12058
|
jsonRepairAttempts++;
|
|
@@ -11554,7 +12063,7 @@ var CliAgentAiService = class {
|
|
|
11554
12063
|
case "prose":
|
|
11555
12064
|
break;
|
|
11556
12065
|
}
|
|
11557
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
12066
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
11558
12067
|
}
|
|
11559
12068
|
} finally {
|
|
11560
12069
|
this.activeAbort = null;
|
|
@@ -11574,6 +12083,7 @@ var CliAgentAiService = class {
|
|
|
11574
12083
|
let text = "";
|
|
11575
12084
|
let error;
|
|
11576
12085
|
let stepIndex = 0;
|
|
12086
|
+
let backgroundAgents = 0;
|
|
11577
12087
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
11578
12088
|
[... truncated, total ${value.length} chars]` : value;
|
|
11579
12089
|
const settle = (id, rawOutput, success, outcome) => {
|
|
@@ -11613,6 +12123,9 @@ var CliAgentAiService = class {
|
|
|
11613
12123
|
case "tool_result":
|
|
11614
12124
|
settle(event.id, event.output, event.success, event.success ? "success" : "failure");
|
|
11615
12125
|
return;
|
|
12126
|
+
case "background_agent":
|
|
12127
|
+
backgroundAgents++;
|
|
12128
|
+
return;
|
|
11616
12129
|
case "permission_request": {
|
|
11617
12130
|
const mapped = mapAgentTool(event.name);
|
|
11618
12131
|
const args = JSON.stringify({ detail: event.detail });
|
|
@@ -11643,7 +12156,7 @@ var CliAgentAiService = class {
|
|
|
11643
12156
|
adapter.dispose();
|
|
11644
12157
|
this.adapter = null;
|
|
11645
12158
|
}
|
|
11646
|
-
return { text, researchLog, error, aborted: signal?.aborted };
|
|
12159
|
+
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
11647
12160
|
}
|
|
11648
12161
|
// --- Process lifecycle ---
|
|
11649
12162
|
async startAdapter(opts) {
|
|
@@ -11702,10 +12215,7 @@ var CliAgentAiService = class {
|
|
|
11702
12215
|
oneShotAdapter = await this.startAdapter(startOptions);
|
|
11703
12216
|
this.conversation = {
|
|
11704
12217
|
startOptions,
|
|
11705
|
-
runners: []
|
|
11706
|
-
prdEnabled: false,
|
|
11707
|
-
prdCaptured: false,
|
|
11708
|
-
prdNudgeSent: false
|
|
12218
|
+
runners: []
|
|
11709
12219
|
};
|
|
11710
12220
|
const turn = await this.runTurn(
|
|
11711
12221
|
"Follow the instructions in your system prompt and produce the plan now.",
|
|
@@ -11722,8 +12232,8 @@ var CliAgentAiService = class {
|
|
|
11722
12232
|
this.lastNativeSessionId = previousSessionId;
|
|
11723
12233
|
}
|
|
11724
12234
|
}
|
|
11725
|
-
async researchAndPlan(userDescription, runners, modelsByRunner,
|
|
11726
|
-
const contextStr = await collectResearchContext(
|
|
12235
|
+
async researchAndPlan(userDescription, runners, modelsByRunner, fs12, onProgress, _fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
12236
|
+
const contextStr = await collectResearchContext(fs12, runners);
|
|
11727
12237
|
const researchLog = [
|
|
11728
12238
|
{ id: `up-${Date.now()}`, type: "user_prompt", content: userDescription, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
11729
12239
|
];
|
|
@@ -12046,6 +12556,181 @@ function retargetTaskRunner(task, runner, catalog) {
|
|
|
12046
12556
|
return { assignedRunner: runner, ...runnerAssignment(catalog, task) };
|
|
12047
12557
|
}
|
|
12048
12558
|
|
|
12559
|
+
// src/services/SkillsService.ts
|
|
12560
|
+
var fs4 = __toESM(require("fs"));
|
|
12561
|
+
var path11 = __toESM(require("path"));
|
|
12562
|
+
|
|
12563
|
+
// src/services/builtinSkills.ts
|
|
12564
|
+
var path10 = __toESM(require("path"));
|
|
12565
|
+
var import_url = require("url");
|
|
12566
|
+
var import_meta = {};
|
|
12567
|
+
function builtinSkillsDir() {
|
|
12568
|
+
const dir = typeof __dirname !== "undefined" && __dirname ? __dirname : path10.dirname((0, import_url.fileURLToPath)(
|
|
12569
|
+
// @ts-expect-error - import.meta is only valid under an ESM module setting
|
|
12570
|
+
import_meta.url
|
|
12571
|
+
));
|
|
12572
|
+
return path10.join(dir, "..", "skills");
|
|
12573
|
+
}
|
|
12574
|
+
|
|
12575
|
+
// src/services/SkillsService.ts
|
|
12576
|
+
var BUILTIN_SKILL_NAMES = ["grilling", "to-spec"];
|
|
12577
|
+
var RETIRED_BUILTIN_SKILL_NAMES = ["grill-me"];
|
|
12578
|
+
function parseSkillFile(filePath, name, source) {
|
|
12579
|
+
const raw = fs4.readFileSync(filePath, "utf8");
|
|
12580
|
+
const frontmatter = {};
|
|
12581
|
+
let content = raw;
|
|
12582
|
+
if (raw.startsWith("---\n")) {
|
|
12583
|
+
const end = raw.indexOf("\n---", 4);
|
|
12584
|
+
if (end !== -1) {
|
|
12585
|
+
const fmText = raw.slice(4, end);
|
|
12586
|
+
for (const line of fmText.split("\n")) {
|
|
12587
|
+
const idx = line.indexOf(":");
|
|
12588
|
+
if (idx === -1) continue;
|
|
12589
|
+
const key = line.slice(0, idx).trim();
|
|
12590
|
+
const value = line.slice(idx + 1).trim();
|
|
12591
|
+
if (key === "disable-model-invocation") {
|
|
12592
|
+
frontmatter[key] = value === "true";
|
|
12593
|
+
} else if (key === "name" || key === "description") {
|
|
12594
|
+
frontmatter[key] = value.replace(/^"|"$/g, "");
|
|
12595
|
+
}
|
|
12596
|
+
}
|
|
12597
|
+
content = raw.slice(end + 4).replace(/^\n+/, "");
|
|
12598
|
+
}
|
|
12599
|
+
}
|
|
12600
|
+
const metadata = {
|
|
12601
|
+
name: frontmatter.name ?? name,
|
|
12602
|
+
description: frontmatter.description ?? "",
|
|
12603
|
+
...frontmatter["disable-model-invocation"] !== void 0 ? { disableModelInvocation: frontmatter["disable-model-invocation"] } : {}
|
|
12604
|
+
};
|
|
12605
|
+
return { name, description: metadata.description, metadata, content, path: filePath, source };
|
|
12606
|
+
}
|
|
12607
|
+
function copyDirSync2(src, dest) {
|
|
12608
|
+
fs4.mkdirSync(dest, { recursive: true });
|
|
12609
|
+
const entries = fs4.readdirSync(src, { withFileTypes: true });
|
|
12610
|
+
for (const entry of entries) {
|
|
12611
|
+
const srcPath = path11.join(src, entry.name);
|
|
12612
|
+
const destPath = path11.join(dest, entry.name);
|
|
12613
|
+
if (entry.isDirectory()) copyDirSync2(srcPath, destPath);
|
|
12614
|
+
else fs4.copyFileSync(srcPath, destPath);
|
|
12615
|
+
}
|
|
12616
|
+
}
|
|
12617
|
+
function readDir(dir, source) {
|
|
12618
|
+
if (!fs4.existsSync(dir)) return [];
|
|
12619
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
12620
|
+
const skills = [];
|
|
12621
|
+
for (const entry of entries) {
|
|
12622
|
+
if (!entry.isDirectory()) continue;
|
|
12623
|
+
const skillFile = path11.join(dir, entry.name, "SKILL.md");
|
|
12624
|
+
if (!fs4.existsSync(skillFile)) continue;
|
|
12625
|
+
const parsed = parseSkillFile(skillFile, entry.name, source);
|
|
12626
|
+
if (parsed) skills.push(parsed);
|
|
12627
|
+
}
|
|
12628
|
+
return skills;
|
|
12629
|
+
}
|
|
12630
|
+
var SkillsService = class {
|
|
12631
|
+
constructor(workspaceRoot) {
|
|
12632
|
+
this.workspaceRoot = workspaceRoot;
|
|
12633
|
+
}
|
|
12634
|
+
workspaceRoot;
|
|
12635
|
+
localDir() {
|
|
12636
|
+
return this.workspaceRoot ? path11.join(this.workspaceRoot, ".ordewell", "skills") : void 0;
|
|
12637
|
+
}
|
|
12638
|
+
globalDir() {
|
|
12639
|
+
return path11.join(globalDataDir(), "skills");
|
|
12640
|
+
}
|
|
12641
|
+
seed(name) {
|
|
12642
|
+
const dest = path11.join(this.globalDir(), name);
|
|
12643
|
+
if (fs4.existsSync(dest)) return false;
|
|
12644
|
+
let srcDir;
|
|
12645
|
+
try {
|
|
12646
|
+
srcDir = path11.join(builtinSkillsDir(), name);
|
|
12647
|
+
} catch (err) {
|
|
12648
|
+
console.warn(`Could not locate built-in skills dir: ${String(err)}`);
|
|
12649
|
+
return false;
|
|
12650
|
+
}
|
|
12651
|
+
if (!fs4.existsSync(srcDir)) {
|
|
12652
|
+
console.warn(`Built-in skill not found in package: ${name}`);
|
|
12653
|
+
return false;
|
|
12654
|
+
}
|
|
12655
|
+
copyDirSync2(srcDir, dest);
|
|
12656
|
+
return true;
|
|
12657
|
+
}
|
|
12658
|
+
seedBuiltinSkill(name) {
|
|
12659
|
+
return this.seed(name);
|
|
12660
|
+
}
|
|
12661
|
+
/** Removes a retired built-in's seed from the global dir, but only if it's untouched by the user. */
|
|
12662
|
+
isUnmodifiedRetiredSeed(dir, expectedName) {
|
|
12663
|
+
let entries;
|
|
12664
|
+
try {
|
|
12665
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
12666
|
+
} catch {
|
|
12667
|
+
return false;
|
|
12668
|
+
}
|
|
12669
|
+
if (entries.length !== 1 || entries[0].name !== "SKILL.md" || !entries[0].isFile()) return false;
|
|
12670
|
+
let raw;
|
|
12671
|
+
try {
|
|
12672
|
+
raw = fs4.readFileSync(path11.join(dir, "SKILL.md"), "utf8");
|
|
12673
|
+
} catch {
|
|
12674
|
+
return false;
|
|
12675
|
+
}
|
|
12676
|
+
if (!raw.startsWith("---\n")) return false;
|
|
12677
|
+
const end = raw.indexOf("\n---", 4);
|
|
12678
|
+
if (end === -1) return false;
|
|
12679
|
+
for (const line of raw.slice(4, end).split("\n")) {
|
|
12680
|
+
const idx = line.indexOf(":");
|
|
12681
|
+
if (idx === -1) continue;
|
|
12682
|
+
const key = line.slice(0, idx).trim();
|
|
12683
|
+
if (key === "name") return line.slice(idx + 1).trim().replace(/^"|"$/g, "") === expectedName;
|
|
12684
|
+
}
|
|
12685
|
+
return false;
|
|
12686
|
+
}
|
|
12687
|
+
pruneRetired() {
|
|
12688
|
+
for (const name of RETIRED_BUILTIN_SKILL_NAMES) {
|
|
12689
|
+
const dest = path11.join(this.globalDir(), name);
|
|
12690
|
+
if (!fs4.existsSync(dest)) continue;
|
|
12691
|
+
if (!this.isUnmodifiedRetiredSeed(dest, name)) continue;
|
|
12692
|
+
try {
|
|
12693
|
+
fs4.rmSync(dest, { recursive: true, force: true });
|
|
12694
|
+
} catch (err) {
|
|
12695
|
+
console.warn(`Could not prune retired skill ${name}: ${String(err)}`);
|
|
12696
|
+
}
|
|
12697
|
+
}
|
|
12698
|
+
}
|
|
12699
|
+
findSkill(name) {
|
|
12700
|
+
this.pruneRetired();
|
|
12701
|
+
if (BUILTIN_SKILL_NAMES.includes(name)) this.seed(name);
|
|
12702
|
+
const localDir = this.localDir();
|
|
12703
|
+
if (localDir) {
|
|
12704
|
+
const localFile = path11.join(localDir, name, "SKILL.md");
|
|
12705
|
+
if (fs4.existsSync(localFile)) return parseSkillFile(localFile, name, "local");
|
|
12706
|
+
}
|
|
12707
|
+
const globalFile = path11.join(this.globalDir(), name, "SKILL.md");
|
|
12708
|
+
if (fs4.existsSync(globalFile)) return parseSkillFile(globalFile, name, "global");
|
|
12709
|
+
return void 0;
|
|
12710
|
+
}
|
|
12711
|
+
listSkills() {
|
|
12712
|
+
this.pruneRetired();
|
|
12713
|
+
for (const name of BUILTIN_SKILL_NAMES) this.seed(name);
|
|
12714
|
+
const byName = /* @__PURE__ */ new Map();
|
|
12715
|
+
for (const skill of readDir(this.globalDir(), "global")) {
|
|
12716
|
+
byName.set(skill.name, skill);
|
|
12717
|
+
}
|
|
12718
|
+
const localDir = this.localDir();
|
|
12719
|
+
if (localDir) {
|
|
12720
|
+
for (const skill of readDir(localDir, "local")) {
|
|
12721
|
+
byName.set(skill.name, skill);
|
|
12722
|
+
}
|
|
12723
|
+
}
|
|
12724
|
+
return [...byName.values()];
|
|
12725
|
+
}
|
|
12726
|
+
getSkillContent(name) {
|
|
12727
|
+
return this.findSkill(name)?.content;
|
|
12728
|
+
}
|
|
12729
|
+
};
|
|
12730
|
+
function createSkillsService(workspaceRoot) {
|
|
12731
|
+
return new SkillsService(workspaceRoot);
|
|
12732
|
+
}
|
|
12733
|
+
|
|
12049
12734
|
// src/services/SessionMessage.ts
|
|
12050
12735
|
function serializeTask(t) {
|
|
12051
12736
|
return {
|
|
@@ -12067,11 +12752,12 @@ function serializeTask(t) {
|
|
|
12067
12752
|
userStoriesCovered: t.userStoriesCovered || void 0
|
|
12068
12753
|
};
|
|
12069
12754
|
}
|
|
12070
|
-
function serializeTaskStatus(t) {
|
|
12755
|
+
function serializeTaskStatus(t, idleSince = null) {
|
|
12071
12756
|
return {
|
|
12072
12757
|
id: t.id,
|
|
12073
12758
|
status: t.status,
|
|
12074
|
-
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null
|
|
12759
|
+
verdict: t.verdict ? { outcome: t.verdict.outcome, reason: t.verdict.reason, checks: t.verdict.checks || [] } : null,
|
|
12760
|
+
idleSince
|
|
12075
12761
|
};
|
|
12076
12762
|
}
|
|
12077
12763
|
function serializePlan(plan) {
|
|
@@ -12093,27 +12779,27 @@ function executionSummary(tasks) {
|
|
|
12093
12779
|
}
|
|
12094
12780
|
|
|
12095
12781
|
// src/utils/sessionStore.ts
|
|
12096
|
-
var
|
|
12097
|
-
var
|
|
12782
|
+
var fs6 = __toESM(require("fs"));
|
|
12783
|
+
var path13 = __toESM(require("path"));
|
|
12098
12784
|
|
|
12099
12785
|
// src/utils/fsHelpers.ts
|
|
12100
|
-
var
|
|
12101
|
-
var
|
|
12786
|
+
var fs5 = __toESM(require("fs"));
|
|
12787
|
+
var path12 = __toESM(require("path"));
|
|
12102
12788
|
var STATE_DIR = ".ordewell";
|
|
12103
12789
|
function ensureDir(dir) {
|
|
12104
|
-
if (!
|
|
12105
|
-
|
|
12790
|
+
if (!fs5.existsSync(dir)) {
|
|
12791
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
12106
12792
|
}
|
|
12107
12793
|
}
|
|
12108
12794
|
function getStateDir(baseDir) {
|
|
12109
|
-
return
|
|
12795
|
+
return path12.join(baseDir ?? process.cwd(), STATE_DIR);
|
|
12110
12796
|
}
|
|
12111
12797
|
var IGNORE_FILE = ".gitignore";
|
|
12112
12798
|
function ensureStateDirIgnored(baseDir) {
|
|
12113
12799
|
const dir = getStateDir(baseDir);
|
|
12114
12800
|
ensureDir(dir);
|
|
12115
12801
|
try {
|
|
12116
|
-
|
|
12802
|
+
fs5.writeFileSync(path12.join(dir, IGNORE_FILE), "*\n", { flag: "wx" });
|
|
12117
12803
|
} catch (err) {
|
|
12118
12804
|
if (err.code !== "EEXIST") throw err;
|
|
12119
12805
|
}
|
|
@@ -12128,7 +12814,7 @@ function mintSessionId() {
|
|
|
12128
12814
|
// src/utils/sessionStore.ts
|
|
12129
12815
|
var SESSIONS_SUBDIR = "sessions";
|
|
12130
12816
|
function getSessionsDir(baseDir) {
|
|
12131
|
-
return
|
|
12817
|
+
return path13.join(getStateDir(baseDir), SESSIONS_SUBDIR);
|
|
12132
12818
|
}
|
|
12133
12819
|
var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
|
|
12134
12820
|
function hasScalarRunner(parsed) {
|
|
@@ -12163,17 +12849,17 @@ function saveSession(plan, goal, baseDir, id) {
|
|
|
12163
12849
|
plan: JSON.parse(JSON.stringify(plan))
|
|
12164
12850
|
};
|
|
12165
12851
|
const filename = sessionFilename(meta);
|
|
12166
|
-
const filePath =
|
|
12167
|
-
|
|
12852
|
+
const filePath = path13.join(sessionsDir, filename);
|
|
12853
|
+
fs6.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
12168
12854
|
return meta;
|
|
12169
12855
|
}
|
|
12170
12856
|
function listSessions(baseDir, logger = defaultLogger) {
|
|
12171
12857
|
const sessionsDir = getSessionsDir(baseDir);
|
|
12172
|
-
if (!
|
|
12173
|
-
const files =
|
|
12858
|
+
if (!fs6.existsSync(sessionsDir)) return [];
|
|
12859
|
+
const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
12174
12860
|
return files.map((f) => {
|
|
12175
12861
|
try {
|
|
12176
|
-
const raw =
|
|
12862
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, f), "utf-8");
|
|
12177
12863
|
const parsed = JSON.parse(raw);
|
|
12178
12864
|
if (hasScalarRunner(parsed)) {
|
|
12179
12865
|
logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
|
|
@@ -12188,10 +12874,10 @@ function listSessions(baseDir, logger = defaultLogger) {
|
|
|
12188
12874
|
}).filter((m) => m !== null).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
12189
12875
|
}
|
|
12190
12876
|
function findSessionFile(sessionsDir, sessionId, logger) {
|
|
12191
|
-
const files =
|
|
12877
|
+
const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
12192
12878
|
for (const f of files) {
|
|
12193
12879
|
try {
|
|
12194
|
-
const raw =
|
|
12880
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, f), "utf-8");
|
|
12195
12881
|
const session = JSON.parse(raw);
|
|
12196
12882
|
if (session.meta.id === sessionId) return f;
|
|
12197
12883
|
} catch (err) {
|
|
@@ -12202,11 +12888,11 @@ function findSessionFile(sessionsDir, sessionId, logger) {
|
|
|
12202
12888
|
}
|
|
12203
12889
|
function loadSession(sessionId, baseDir, logger = defaultLogger) {
|
|
12204
12890
|
const sessionsDir = getSessionsDir(baseDir);
|
|
12205
|
-
if (!
|
|
12891
|
+
if (!fs6.existsSync(sessionsDir)) return null;
|
|
12206
12892
|
try {
|
|
12207
12893
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
12208
12894
|
if (file) {
|
|
12209
|
-
const raw =
|
|
12895
|
+
const raw = fs6.readFileSync(path13.join(sessionsDir, file), "utf-8");
|
|
12210
12896
|
const parsed = JSON.parse(raw);
|
|
12211
12897
|
if (hasScalarRunner(parsed)) {
|
|
12212
12898
|
logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
|
|
@@ -12236,11 +12922,11 @@ function getLatestSession(baseDir, logger = defaultLogger) {
|
|
|
12236
12922
|
}
|
|
12237
12923
|
function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
12238
12924
|
const sessionsDir = getSessionsDir(baseDir);
|
|
12239
|
-
if (!
|
|
12925
|
+
if (!fs6.existsSync(sessionsDir)) return false;
|
|
12240
12926
|
try {
|
|
12241
12927
|
const file = findSessionFile(sessionsDir, sessionId, logger);
|
|
12242
12928
|
if (file) {
|
|
12243
|
-
|
|
12929
|
+
fs6.unlinkSync(path13.join(sessionsDir, file));
|
|
12244
12930
|
return true;
|
|
12245
12931
|
}
|
|
12246
12932
|
} catch (err) {
|
|
@@ -12249,7 +12935,36 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
12249
12935
|
return false;
|
|
12250
12936
|
}
|
|
12251
12937
|
|
|
12938
|
+
// src/utils/prdStore.ts
|
|
12939
|
+
var fs7 = __toESM(require("fs"));
|
|
12940
|
+
var path14 = __toESM(require("path"));
|
|
12941
|
+
var PRD_BLOCK_RE = /<!--\s*ORDEWELL_PRD_START\s+slug="([^"]+)"\s*-->\s*([\s\S]*?)\s*<!--\s*ORDEWELL_PRD_END\s*-->/;
|
|
12942
|
+
function extractPrdBlock(text) {
|
|
12943
|
+
const match = PRD_BLOCK_RE.exec(text);
|
|
12944
|
+
if (!match) return null;
|
|
12945
|
+
const slug2 = sanitizeSlug(match[1]);
|
|
12946
|
+
const markdown = match[2].trim();
|
|
12947
|
+
if (!slug2 || !markdown) return null;
|
|
12948
|
+
return { slug: slug2, markdown };
|
|
12949
|
+
}
|
|
12950
|
+
function sanitizeSlug(raw) {
|
|
12951
|
+
return raw.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
12952
|
+
}
|
|
12953
|
+
function savePrdMarkdown(workspace, slug2, markdown) {
|
|
12954
|
+
const dir = path14.join(workspace, ".scratch", sanitizeSlug(slug2));
|
|
12955
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
12956
|
+
const file = path14.join(dir, "PRD.md");
|
|
12957
|
+
fs7.writeFileSync(file, markdown.endsWith("\n") ? markdown : markdown + "\n", "utf8");
|
|
12958
|
+
return file;
|
|
12959
|
+
}
|
|
12960
|
+
|
|
12252
12961
|
// src/services/createSession.ts
|
|
12962
|
+
var CATALOG_MODEL_CAP = 100;
|
|
12963
|
+
var MAX_TASK_QUERIES = 3;
|
|
12964
|
+
var MAX_TASK_QUERIES_HARD = 6;
|
|
12965
|
+
function freshReadBudget() {
|
|
12966
|
+
return { answered: 0, seen: /* @__PURE__ */ new Set() };
|
|
12967
|
+
}
|
|
12253
12968
|
var PlanEditError = class extends Error {
|
|
12254
12969
|
constructor(message) {
|
|
12255
12970
|
super(message);
|
|
@@ -12259,6 +12974,17 @@ var PlanEditError = class extends Error {
|
|
|
12259
12974
|
function sessionRuntimeSettings(settings) {
|
|
12260
12975
|
return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
|
|
12261
12976
|
}
|
|
12977
|
+
function resolveSkillInvocation(text, skillsService) {
|
|
12978
|
+
const match = text.trim().match(/^\/([a-z][a-z0-9_-]*)$/im);
|
|
12979
|
+
if (!match) return text;
|
|
12980
|
+
const skillName = match[1].toLowerCase();
|
|
12981
|
+
const skill = skillsService.findSkill(skillName);
|
|
12982
|
+
if (!skill) {
|
|
12983
|
+
const available = typeof skillsService.listSkills === "function" ? skillsService.listSkills().map((s) => s.name).join(", ") : "";
|
|
12984
|
+
return `Unknown skill: ${skillName}. Available skills: ${available}`;
|
|
12985
|
+
}
|
|
12986
|
+
return skill.content;
|
|
12987
|
+
}
|
|
12262
12988
|
var Session = class {
|
|
12263
12989
|
/** Injected by a test; when present it is the service, forever. */
|
|
12264
12990
|
pinnedAiService;
|
|
@@ -12286,6 +13012,7 @@ var Session = class {
|
|
|
12286
13012
|
unsubObserver = null;
|
|
12287
13013
|
hostSessionId;
|
|
12288
13014
|
currentSessionId;
|
|
13015
|
+
skillsService;
|
|
12289
13016
|
constructor(deps) {
|
|
12290
13017
|
this.config = deps.config;
|
|
12291
13018
|
this.registry = deps.registry;
|
|
@@ -12304,6 +13031,7 @@ var Session = class {
|
|
|
12304
13031
|
this.settingsFn = deps.settings;
|
|
12305
13032
|
this.hostSessionId = deps.sessionId;
|
|
12306
13033
|
this.currentSessionId = deps.sessionId ?? mintSessionId();
|
|
13034
|
+
this.skillsService = deps.skillsService ?? new SkillsService(this.workspaceRootFn());
|
|
12307
13035
|
this.approvals = new PendingApprovals({
|
|
12308
13036
|
onRequest: ({ id, request }) => this.broadcast({
|
|
12309
13037
|
type: "approval_request",
|
|
@@ -12398,7 +13126,7 @@ var Session = class {
|
|
|
12398
13126
|
onTaskChanged: () => {
|
|
12399
13127
|
if (!this.plan) return;
|
|
12400
13128
|
const tasks = this.store.allTasks;
|
|
12401
|
-
this.broadcast({ type: "status_update", tasks: tasks.map(serializeTaskStatus) });
|
|
13129
|
+
this.broadcast({ type: "status_update", tasks: tasks.map((t) => serializeTaskStatus(t, this.orchestrator.getIdleSince(t.id))) });
|
|
12402
13130
|
},
|
|
12403
13131
|
onQueueReady: () => {
|
|
12404
13132
|
this.broadcast({ type: "queue_ready" });
|
|
@@ -12416,7 +13144,7 @@ var Session = class {
|
|
|
12416
13144
|
onTick: () => {
|
|
12417
13145
|
if (!this.plan) return;
|
|
12418
13146
|
const tasks = this.store.allTasks;
|
|
12419
|
-
this.broadcast({ type: "status_update", tasks: tasks.map(serializeTaskStatus) });
|
|
13147
|
+
this.broadcast({ type: "status_update", tasks: tasks.map((t) => serializeTaskStatus(t, this.orchestrator.getIdleSince(t.id))) });
|
|
12420
13148
|
},
|
|
12421
13149
|
onExecutionComplete: () => {
|
|
12422
13150
|
if (!this.plan) return;
|
|
@@ -12486,6 +13214,21 @@ var Session = class {
|
|
|
12486
13214
|
runnerModesFor(runners) {
|
|
12487
13215
|
return runnerModesFrom(this.registry, runners);
|
|
12488
13216
|
}
|
|
13217
|
+
/**
|
|
13218
|
+
* The catalog a model/task-mode edit is checked against — the same
|
|
13219
|
+
* discovered models and manifest modes the per-turn catalog block shows the
|
|
13220
|
+
* planner, so a refusal here can never name something invalid that the
|
|
13221
|
+
* planner was never told about. `modelsCache` is read live, not filtered by
|
|
13222
|
+
* the allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
13223
|
+
* allowlist itself, the same way `coerceAssignments` does.
|
|
13224
|
+
*/
|
|
13225
|
+
editCatalog() {
|
|
13226
|
+
return {
|
|
13227
|
+
modelsByRunner: this.modelsCache,
|
|
13228
|
+
runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
|
|
13229
|
+
perRunnerAllowlist: this.settingsFn().modelAllowlist ?? this.currentAllowlist
|
|
13230
|
+
};
|
|
13231
|
+
}
|
|
12489
13232
|
get planState() {
|
|
12490
13233
|
return this.plan;
|
|
12491
13234
|
}
|
|
@@ -12613,7 +13356,7 @@ var Session = class {
|
|
|
12613
13356
|
*/
|
|
12614
13357
|
async startPlanning(goal, runners, options) {
|
|
12615
13358
|
this.plan = null;
|
|
12616
|
-
this.goal = goal;
|
|
13359
|
+
this.goal = this.resolveSkillInvocation(goal);
|
|
12617
13360
|
this.remintSessionId();
|
|
12618
13361
|
this.beginFreshPlan();
|
|
12619
13362
|
const enabled = this.config.enabledRunners;
|
|
@@ -12622,7 +13365,7 @@ var Session = class {
|
|
|
12622
13365
|
const modelsByRunner = await this.modelResolver.modelsForRunners(chosenRunners);
|
|
12623
13366
|
this.modelsCache = modelsByRunner;
|
|
12624
13367
|
const runnerModes = this.runnerModesFor(chosenRunners);
|
|
12625
|
-
const {
|
|
13368
|
+
const { verificationEnabled, modelAllowlist } = this.settingsFn();
|
|
12626
13369
|
this.currentAllowlist = modelAllowlist ?? {};
|
|
12627
13370
|
const filteredModels = filterModelsForPrompt(modelsByRunner, this.currentAllowlist);
|
|
12628
13371
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -12632,22 +13375,18 @@ var Session = class {
|
|
|
12632
13375
|
status: "draft",
|
|
12633
13376
|
runners: chosenRunners,
|
|
12634
13377
|
lastUpdated: now,
|
|
12635
|
-
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: goal, timestamp: now }],
|
|
12636
|
-
conversationHistory: [{ role: "user", content: goal, timestamp: now }]
|
|
13378
|
+
researchLog: [{ id: `up-${Date.now()}`, type: "user_prompt", content: this.goal, timestamp: now }],
|
|
13379
|
+
conversationHistory: [{ role: "user", content: this.goal, timestamp: now }]
|
|
12637
13380
|
};
|
|
12638
13381
|
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
12639
13382
|
try {
|
|
12640
13383
|
const turn = await this.aiService.startConversation({
|
|
12641
|
-
goal,
|
|
13384
|
+
goal: this.goal,
|
|
12642
13385
|
runners: chosenRunners,
|
|
12643
13386
|
modelsByRunner: filteredModels,
|
|
12644
13387
|
runnerModes,
|
|
12645
13388
|
autonomousDefault: this.config.autonomousMode,
|
|
12646
|
-
grillMeEnabled: grillMeEnabled ?? false,
|
|
12647
|
-
prdEnabled: prdEnabled ?? false,
|
|
12648
|
-
reviewEnabled: reviewEnabled ?? false,
|
|
12649
13389
|
verificationEnabled: verificationEnabled ?? false,
|
|
12650
|
-
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
12651
13390
|
fs: this.fsAdapter,
|
|
12652
13391
|
fetcher: this.fetcher,
|
|
12653
13392
|
onProgress: (p) => this.translateProgress(p),
|
|
@@ -12659,19 +13398,22 @@ var Session = class {
|
|
|
12659
13398
|
}
|
|
12660
13399
|
}
|
|
12661
13400
|
/**
|
|
12662
|
-
* Every subsequent user reply in the planning conversation —
|
|
12663
|
-
* answers,
|
|
13401
|
+
* Every subsequent user reply in the planning conversation — clarifying
|
|
13402
|
+
* answers, outline confirm. One branch, no phase ladder.
|
|
12664
13403
|
*/
|
|
12665
13404
|
async continueConversation(userMessage, options) {
|
|
12666
13405
|
if (!this.plan) throw new Error("No planning conversation to continue");
|
|
12667
13406
|
const priorHistory = this.plan.conversationHistory ?? [];
|
|
13407
|
+
const priorResearchLog = this.plan.researchLog ?? [];
|
|
12668
13408
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12669
|
-
this.
|
|
12670
|
-
this.
|
|
12671
|
-
const
|
|
13409
|
+
const resolved = this.resolveSkillInvocation(userMessage);
|
|
13410
|
+
this.appendTranscript("user", resolved, now);
|
|
13411
|
+
const historyAfterOwnAppend = this.plan.conversationHistory;
|
|
13412
|
+
this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: resolved, timestamp: now }];
|
|
13413
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock()].filter(Boolean).join("\n\n");
|
|
12672
13414
|
const outgoing = contextBlock ? `${contextBlock}
|
|
12673
13415
|
|
|
12674
|
-
${
|
|
13416
|
+
${resolved}` : resolved;
|
|
12675
13417
|
const aiService = this.aiService;
|
|
12676
13418
|
const canContinueLive = aiService.hasActiveConversation() && (aiService.conversationMatchesConfig?.() ?? true);
|
|
12677
13419
|
const releaseAbort = this.denyApprovalsOnAbort(options?.signal);
|
|
@@ -12681,6 +13423,8 @@ ${userMessage}` : userMessage;
|
|
|
12681
13423
|
(p) => this.translateProgress(p),
|
|
12682
13424
|
options?.signal
|
|
12683
13425
|
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
13426
|
+
const reads = freshReadBudget();
|
|
13427
|
+
turn = await this.drainTaskQueries(turn, reads, options);
|
|
12684
13428
|
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
12685
13429
|
this.queueMessage(userMessage);
|
|
12686
13430
|
this.plan.queuedMessages = this.getQueuedMessages();
|
|
@@ -12690,11 +13434,74 @@ ${userMessage}` : userMessage;
|
|
|
12690
13434
|
researchLog: turn.researchLog
|
|
12691
13435
|
};
|
|
12692
13436
|
}
|
|
12693
|
-
return await this.settleTurn(turn, options);
|
|
13437
|
+
return await this.settleTurn(turn, options, reads);
|
|
13438
|
+
} catch (err) {
|
|
13439
|
+
if (this.plan && this.plan.conversationHistory === historyAfterOwnAppend) {
|
|
13440
|
+
this.plan.conversationHistory = priorHistory;
|
|
13441
|
+
this.plan.researchLog = priorResearchLog;
|
|
13442
|
+
}
|
|
13443
|
+
throw err;
|
|
12694
13444
|
} finally {
|
|
12695
13445
|
releaseAbort();
|
|
12696
13446
|
}
|
|
12697
13447
|
}
|
|
13448
|
+
/**
|
|
13449
|
+
* Answer every read the planner emits until it says something else.
|
|
13450
|
+
*
|
|
13451
|
+
* The channel is a text envelope rather than a registered tool because the
|
|
13452
|
+
* protocol has to be identical on both planner backends (ADR-0009): Ordewell
|
|
13453
|
+
* owns a tool loop only in the API case, and a harness planner running as a
|
|
13454
|
+
* coding-agent subprocess can only be reached this way.
|
|
13455
|
+
*
|
|
13456
|
+
* Draining here — outside {@link repairLoop} — is what keeps reads free of
|
|
13457
|
+
* the repair budget. A planner that looks a task up and *then* fumbles its
|
|
13458
|
+
* ops JSON still gets its two corrective retries; charging it for the read
|
|
13459
|
+
* would cost it the chance to fix the edit.
|
|
13460
|
+
*/
|
|
13461
|
+
async drainTaskQueries(turn, reads, options) {
|
|
13462
|
+
const carried = [];
|
|
13463
|
+
let current = turn;
|
|
13464
|
+
while (current.kind === "task_query") {
|
|
13465
|
+
carried.push(...current.researchLog);
|
|
13466
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD || !this.aiService.hasActiveConversation() || options?.signal?.aborted) {
|
|
13467
|
+
return {
|
|
13468
|
+
kind: "message",
|
|
13469
|
+
text: "The planner kept asking to read tasks instead of answering. Nothing was changed \u2014 ask again, or be more specific about the edit you want.",
|
|
13470
|
+
researchLog: carried
|
|
13471
|
+
};
|
|
13472
|
+
}
|
|
13473
|
+
const signature = taskQuerySignature(current.query);
|
|
13474
|
+
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
13475
|
+
reads.seen.add(signature);
|
|
13476
|
+
reads.answered++;
|
|
13477
|
+
const answer = this.taskQueryAnswer(current.query);
|
|
13478
|
+
current = await this.aiService.continueConversation(
|
|
13479
|
+
insist ? `${answer}
|
|
13480
|
+
|
|
13481
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer,
|
|
13482
|
+
(p) => this.translateProgress(p),
|
|
13483
|
+
options?.signal
|
|
13484
|
+
);
|
|
13485
|
+
}
|
|
13486
|
+
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
13487
|
+
}
|
|
13488
|
+
/**
|
|
13489
|
+
* Render one read out of live state. Never persisted to the transcript: the
|
|
13490
|
+
* detail is context for the planner's next reply, and re-sending it on every
|
|
13491
|
+
* later turn is exactly the cost this channel exists to avoid.
|
|
13492
|
+
*/
|
|
13493
|
+
taskQueryAnswer(query) {
|
|
13494
|
+
const runners = this.plan?.runners ?? [];
|
|
13495
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13496
|
+
return renderTaskQueryAnswer(query, this.store.planTasks, {
|
|
13497
|
+
runners,
|
|
13498
|
+
// Allowlist-filtered like the always-on block: a read must not offer a
|
|
13499
|
+
// model the planner is forbidden to assign.
|
|
13500
|
+
models: filterModelsForPrompt(this.modelsCache, allowlist),
|
|
13501
|
+
modes: this.runnerModesFor(runners),
|
|
13502
|
+
autonomousDefault: this.config.autonomousMode
|
|
13503
|
+
});
|
|
13504
|
+
}
|
|
12698
13505
|
/**
|
|
12699
13506
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
12700
13507
|
* atomically; validation failures are fed back to the model for up to 2
|
|
@@ -12702,7 +13509,7 @@ ${userMessage}` : userMessage;
|
|
|
12702
13509
|
* first turn (startPlanning) and every later turn route through here — one
|
|
12703
13510
|
* path, not two.
|
|
12704
13511
|
*/
|
|
12705
|
-
async settleTurn(turn, options) {
|
|
13512
|
+
async settleTurn(turn, options, reads = freshReadBudget()) {
|
|
12706
13513
|
const invalidOps = (errors, researchLog) => ({
|
|
12707
13514
|
turn: {
|
|
12708
13515
|
kind: "message",
|
|
@@ -12714,11 +13521,15 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12714
13521
|
}
|
|
12715
13522
|
});
|
|
12716
13523
|
const settled = await repairLoop({
|
|
12717
|
-
first: () => turn,
|
|
12718
|
-
resend: (corrective) => this.
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
13524
|
+
first: () => this.drainTaskQueries(turn, reads, options),
|
|
13525
|
+
resend: async (corrective) => this.drainTaskQueries(
|
|
13526
|
+
await this.aiService.continueConversation(
|
|
13527
|
+
corrective,
|
|
13528
|
+
(p) => this.translateProgress(p),
|
|
13529
|
+
options?.signal
|
|
13530
|
+
),
|
|
13531
|
+
reads,
|
|
13532
|
+
options
|
|
12722
13533
|
),
|
|
12723
13534
|
interpret: (t) => {
|
|
12724
13535
|
if (t.kind !== "task_ops") return { done: { turn: t } };
|
|
@@ -12738,6 +13549,52 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12738
13549
|
}
|
|
12739
13550
|
return this.applyConversationTurn(settled.turn);
|
|
12740
13551
|
}
|
|
13552
|
+
/**
|
|
13553
|
+
* The catalog the planner may actually draw from — model ids and task-mode
|
|
13554
|
+
* ids, per selected runner — emitted on EVERY turn, before any plan exists
|
|
13555
|
+
* or after. The system prompt shows this once at conversation start; a long
|
|
13556
|
+
* clarifying conversation outlives that single showing and the planner
|
|
13557
|
+
* starts misquoting it, so this re-states it per turn instead.
|
|
13558
|
+
*
|
|
13559
|
+
* Built from the allowlist-filtered view (`filterModelsForPrompt`), the same
|
|
13560
|
+
* one the system prompt used — a restricted allowlist stays a hard bound on
|
|
13561
|
+
* every turn, not just the first. `this.modelsCache` itself is never
|
|
13562
|
+
* filtered: `coerceAssignments` needs the full discovered catalog alongside
|
|
13563
|
+
* the allowlist to resolve labels and clamp effort to real variants.
|
|
13564
|
+
*
|
|
13565
|
+
* Reads `this.plan.runners` live, so a runner admitted mid-session by a
|
|
13566
|
+
* retarget (`admitRunner`) is filtered and shown like every other — there is
|
|
13567
|
+
* no separate "originally selected" list to fall stale.
|
|
13568
|
+
*/
|
|
13569
|
+
catalogBlock() {
|
|
13570
|
+
if (!this.plan) return null;
|
|
13571
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13572
|
+
const filteredModels = filterModelsForPrompt(this.modelsCache, allowlist);
|
|
13573
|
+
const runnerModes = this.runnerModesFor(this.plan.runners);
|
|
13574
|
+
const autonomousDefault = this.config.autonomousMode;
|
|
13575
|
+
const modelLines = this.plan.runners.map((runner) => {
|
|
13576
|
+
const models = filteredModels[runner] ?? [];
|
|
13577
|
+
const capped = models.slice(0, CATALOG_MODEL_CAP);
|
|
13578
|
+
const remainder = models.length - capped.length;
|
|
13579
|
+
const ids = capped.map((m) => m.modelId).join(", ") || "(no models discovered)";
|
|
13580
|
+
return `${runner}: ${ids}${remainder > 0 ? ` \u2026 +${remainder} more not shown` : ""}`;
|
|
13581
|
+
});
|
|
13582
|
+
const modeLines = this.plan.runners.map((runner) => {
|
|
13583
|
+
const modes = runnerModes[runner] ?? [];
|
|
13584
|
+
if (modes.length === 0) return `${runner}: (no modes declared)`;
|
|
13585
|
+
const defaultId = resolveDefaultMode(modes, autonomousDefault);
|
|
13586
|
+
const ids = modes.map((m) => `${m.id}${m.id === defaultId ? " (default)" : ""}`).join(", ");
|
|
13587
|
+
return `${runner}: ${ids}`;
|
|
13588
|
+
});
|
|
13589
|
+
return [
|
|
13590
|
+
"<available_models>",
|
|
13591
|
+
...modelLines,
|
|
13592
|
+
"</available_models>",
|
|
13593
|
+
"<available_task_modes>",
|
|
13594
|
+
...modeLines,
|
|
13595
|
+
"</available_task_modes>"
|
|
13596
|
+
].join("\n");
|
|
13597
|
+
}
|
|
12741
13598
|
/**
|
|
12742
13599
|
* The "you are here" block for post-plan chat: current tasks with stable
|
|
12743
13600
|
* references, plus the task-ops protocol. Injected per turn (never
|
|
@@ -12747,44 +13604,48 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
12747
13604
|
planContextBlock() {
|
|
12748
13605
|
if (!this.plan || this.store.planTasks.length === 0) return null;
|
|
12749
13606
|
const orderOf = new Map(this.store.planTasks.map((t) => [t.id, `#${t.order}`]));
|
|
12750
|
-
const lines = this.store.planTasks.map(
|
|
12751
|
-
|
|
12752
|
-
|
|
13607
|
+
const lines = this.store.planTasks.map((t) => {
|
|
13608
|
+
const isMan = t.type === "user";
|
|
13609
|
+
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
13610
|
+
return `#${t.order} id=${t.id} "${t.title}" [${t.status}] type:${isMan ? "MAN" : "AI"} runner:${t.assignedRunner} ${runFields} autonomy:${t.autonomy ?? "-"} slice:${t.sliceType ?? "-"} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`;
|
|
13611
|
+
});
|
|
12753
13612
|
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
12754
13613
|
const execNote = this.hasLiveWork ? `
|
|
12755
13614
|
Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
|
|
12756
|
-
const
|
|
12757
|
-
for (const runner of this.plan.runners) {
|
|
12758
|
-
const models = this.modelsCache[runner] ?? [];
|
|
12759
|
-
const ids = models.map((m) => m.modelId).join(", ");
|
|
12760
|
-
modelLines.push(`${runner}: ${ids || "(no models discovered)"}`);
|
|
12761
|
-
}
|
|
12762
|
-
const modelBlock = modelLines.length > 0 ? ["<available_models>", ...modelLines, "</available_models>"].join("\n") : "";
|
|
13615
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
12763
13616
|
return [
|
|
12764
13617
|
"<current_plan>",
|
|
12765
13618
|
...lines,
|
|
12766
13619
|
"</current_plan>",
|
|
12767
|
-
|
|
12768
|
-
"The block above is the CURRENT task plan. Choose how to respond:",
|
|
13620
|
+
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
12769
13621
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
13622
|
+
// The reminder text is owned by TaskQuery beside the parser, so the
|
|
13623
|
+
// protocol the planner reads and the one Ordewell accepts stay one thing.
|
|
13624
|
+
TASK_QUERY_REMINDER,
|
|
12770
13625
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
12771
13626
|
' {"taskOps": [',
|
|
12772
|
-
|
|
12773
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?}},',
|
|
13627
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
13628
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
12774
13629
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
12775
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]},',
|
|
12776
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}},',
|
|
12777
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...]}',
|
|
13630
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
13631
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
13632
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
13633
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
12778
13634
|
" ]}",
|
|
12779
13635
|
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
13636
|
+
'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.',
|
|
13637
|
+
'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.',
|
|
12780
13638
|
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
12781
|
-
|
|
13639
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
13640
|
+
'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.' + execNote,
|
|
13641
|
+
'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.',
|
|
13642
|
+
'"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.'
|
|
12782
13643
|
].filter(Boolean).join("\n");
|
|
12783
13644
|
}
|
|
12784
13645
|
/** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
|
|
12785
13646
|
applyTaskOpsTurn(turn) {
|
|
12786
13647
|
if (!this.plan) throw new Error("No active plan state");
|
|
12787
|
-
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners);
|
|
13648
|
+
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners, this.editCatalog());
|
|
12788
13649
|
if (!result.ok) return { errors: result.errors };
|
|
12789
13650
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12790
13651
|
const content = `Tasks updated:
|
|
@@ -12824,7 +13685,7 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
12824
13685
|
const modelsByRunner = await this.modelResolver.modelsForRunners(runners);
|
|
12825
13686
|
this.modelsCache = modelsByRunner;
|
|
12826
13687
|
const runnerModes = this.runnerModesFor(runners);
|
|
12827
|
-
const {
|
|
13688
|
+
const { verificationEnabled, modelAllowlist } = this.settingsFn();
|
|
12828
13689
|
this.currentAllowlist = modelAllowlist ?? {};
|
|
12829
13690
|
const filteredModels = filterModelsForPrompt(modelsByRunner, this.currentAllowlist);
|
|
12830
13691
|
const goal = this.goal || priorHistory.find((m) => m.role === "user")?.content || userMessage;
|
|
@@ -12834,11 +13695,7 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
12834
13695
|
modelsByRunner: filteredModels,
|
|
12835
13696
|
runnerModes,
|
|
12836
13697
|
autonomousDefault: this.config.autonomousMode,
|
|
12837
|
-
grillMeEnabled: grillMeEnabled ?? false,
|
|
12838
|
-
prdEnabled: prdEnabled ?? false,
|
|
12839
|
-
reviewEnabled: reviewEnabled ?? false,
|
|
12840
13698
|
verificationEnabled: verificationEnabled ?? false,
|
|
12841
|
-
researchSubagentsEnabled: researchSubagentsEnabled ?? false,
|
|
12842
13699
|
fs: this.fsAdapter,
|
|
12843
13700
|
fetcher: this.fetcher,
|
|
12844
13701
|
onProgress: (p) => this.translateProgress(p),
|
|
@@ -12851,6 +13708,17 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
12851
13708
|
get isConversationActive() {
|
|
12852
13709
|
return this.aiService.hasActiveConversation();
|
|
12853
13710
|
}
|
|
13711
|
+
/**
|
|
13712
|
+
* Intercept a skill invocation (/skill-name) and substitute the skill's
|
|
13713
|
+
* markdown content BEFORE the message reaches the planner. Prevents runners
|
|
13714
|
+
* (Claude Code, OpenCode) from trying to resolve the skill in their own
|
|
13715
|
+
* directory instead of .ordewell/skills/. An unknown skill is surfaced to
|
|
13716
|
+
* the planner as a notice naming what IS available, rather than passing a
|
|
13717
|
+
* bare /unknown through to be mis-resolved.
|
|
13718
|
+
*/
|
|
13719
|
+
resolveSkillInvocation(text) {
|
|
13720
|
+
return resolveSkillInvocation(text, this.skillsService);
|
|
13721
|
+
}
|
|
12854
13722
|
/** Append one dialogue entry to the persisted transcript. Callers persist via the mutatePlan ritual. */
|
|
12855
13723
|
appendTranscript(role, content, timestamp, kind) {
|
|
12856
13724
|
if (!this.plan) return;
|
|
@@ -13051,15 +13919,22 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
13051
13919
|
return plan;
|
|
13052
13920
|
}
|
|
13053
13921
|
/**
|
|
13054
|
-
* Patch one task's fields. A hand-set dependency list
|
|
13055
|
-
*
|
|
13056
|
-
*
|
|
13057
|
-
*
|
|
13922
|
+
* Patch one task's fields. A hand-set dependency list, or a type flip
|
|
13923
|
+
* between AI and MAN, are the patches that can leave a task incoherent
|
|
13924
|
+
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
13925
|
+
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
13926
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule)
|
|
13927
|
+
* rather than a second copy of the rules — and throws, so the surface can
|
|
13928
|
+
* say why. Gated on the task existing so an edit to an unknown id still
|
|
13929
|
+
* falls through to the no-op `store.update` below instead of throwing.
|
|
13058
13930
|
*/
|
|
13059
13931
|
async updateTask(taskId, changes) {
|
|
13060
|
-
if (changes.dependencies) {
|
|
13061
|
-
const check =
|
|
13062
|
-
if (!check.ok) throw new PlanEditError(check.error ?? "Those
|
|
13932
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode) && this.store.get(taskId)) {
|
|
13933
|
+
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
13934
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
13935
|
+
if (check.clear?.length) {
|
|
13936
|
+
changes = { ...changes, ...Object.fromEntries(check.clear.map((f) => [f, void 0])) };
|
|
13937
|
+
}
|
|
13063
13938
|
}
|
|
13064
13939
|
return this.editPlan(
|
|
13065
13940
|
() => Boolean(this.store.update(taskId, changes)),
|
|
@@ -14059,13 +14934,12 @@ var TmuxRunner = class extends AbstractRunner {
|
|
|
14059
14934
|
// src/utils/daemonToken.ts
|
|
14060
14935
|
var import_crypto2 = require("crypto");
|
|
14061
14936
|
var import_fs2 = require("fs");
|
|
14062
|
-
var import_os2 = require("os");
|
|
14063
14937
|
var import_path2 = require("path");
|
|
14064
14938
|
var BEARER_PREFIX = "bearer ";
|
|
14065
14939
|
var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
|
|
14066
14940
|
var DAEMON_SUBPROTOCOL = "ordewell.v1";
|
|
14067
14941
|
function configDir() {
|
|
14068
|
-
return (
|
|
14942
|
+
return globalDataDir();
|
|
14069
14943
|
}
|
|
14070
14944
|
function daemonTokenPath(port) {
|
|
14071
14945
|
return (0, import_path2.join)(configDir(), `server-${port}.token`);
|
|
@@ -14129,19 +15003,18 @@ function tokensMatch(presented, expected) {
|
|
|
14129
15003
|
}
|
|
14130
15004
|
|
|
14131
15005
|
// src/plugins/RunnerRegistry.ts
|
|
14132
|
-
var
|
|
14133
|
-
var
|
|
14134
|
-
var
|
|
15006
|
+
var fs9 = __toESM(require("fs"));
|
|
15007
|
+
var path17 = __toESM(require("path"));
|
|
15008
|
+
var os4 = __toESM(require("os"));
|
|
14135
15009
|
var import_child_process9 = require("child_process");
|
|
14136
15010
|
var import_crypto3 = require("crypto");
|
|
14137
15011
|
|
|
14138
15012
|
// src/plugins/FsPluginStore.ts
|
|
14139
|
-
var
|
|
14140
|
-
var
|
|
14141
|
-
var os4 = __toESM(require("os"));
|
|
15013
|
+
var fs8 = __toESM(require("fs"));
|
|
15014
|
+
var path16 = __toESM(require("path"));
|
|
14142
15015
|
|
|
14143
15016
|
// src/plugins/pluginNames.ts
|
|
14144
|
-
var
|
|
15017
|
+
var path15 = __toESM(require("path"));
|
|
14145
15018
|
var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
14146
15019
|
var MAX_PLUGIN_NAME_LENGTH = 64;
|
|
14147
15020
|
function isPlainPluginName(name) {
|
|
@@ -14156,10 +15029,10 @@ function assertPlainPluginName(name) {
|
|
|
14156
15029
|
}
|
|
14157
15030
|
function resolvePluginInstallDir(pluginsDir, name) {
|
|
14158
15031
|
assertPlainPluginName(name);
|
|
14159
|
-
const root =
|
|
14160
|
-
const dest =
|
|
14161
|
-
const relative3 =
|
|
14162
|
-
if (relative3 !== name || relative3 === "" ||
|
|
15032
|
+
const root = path15.resolve(pluginsDir);
|
|
15033
|
+
const dest = path15.resolve(root, name);
|
|
15034
|
+
const relative3 = path15.relative(root, dest);
|
|
15035
|
+
if (relative3 !== name || relative3 === "" || path15.isAbsolute(relative3)) {
|
|
14163
15036
|
throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
|
|
14164
15037
|
}
|
|
14165
15038
|
return dest;
|
|
@@ -14174,13 +15047,13 @@ function isValidManifest(obj) {
|
|
|
14174
15047
|
|
|
14175
15048
|
// src/plugins/FsPluginStore.ts
|
|
14176
15049
|
function userPluginsDir() {
|
|
14177
|
-
return
|
|
15050
|
+
return path16.join(globalDataDir(), "plugins");
|
|
14178
15051
|
}
|
|
14179
15052
|
function loadManifestFromDir(dir) {
|
|
14180
|
-
const manifestPath =
|
|
14181
|
-
if (!
|
|
15053
|
+
const manifestPath = path16.join(dir, "manifest.json");
|
|
15054
|
+
if (!fs8.existsSync(manifestPath)) return null;
|
|
14182
15055
|
try {
|
|
14183
|
-
const raw =
|
|
15056
|
+
const raw = fs8.readFileSync(manifestPath, "utf8");
|
|
14184
15057
|
const parsed = JSON.parse(raw);
|
|
14185
15058
|
if (!isValidManifest(parsed)) return null;
|
|
14186
15059
|
return parsed;
|
|
@@ -14188,31 +15061,31 @@ function loadManifestFromDir(dir) {
|
|
|
14188
15061
|
return null;
|
|
14189
15062
|
}
|
|
14190
15063
|
}
|
|
14191
|
-
function
|
|
15064
|
+
function copyDirSync3(src, dest) {
|
|
14192
15065
|
ensureDir(dest);
|
|
14193
|
-
const entries =
|
|
15066
|
+
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
14194
15067
|
for (const entry of entries) {
|
|
14195
|
-
const srcPath =
|
|
14196
|
-
const destPath =
|
|
15068
|
+
const srcPath = path16.join(src, entry.name);
|
|
15069
|
+
const destPath = path16.join(dest, entry.name);
|
|
14197
15070
|
if (entry.isDirectory()) {
|
|
14198
|
-
|
|
15071
|
+
copyDirSync3(srcPath, destPath);
|
|
14199
15072
|
} else {
|
|
14200
|
-
|
|
15073
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
14201
15074
|
}
|
|
14202
15075
|
}
|
|
14203
15076
|
}
|
|
14204
15077
|
function rmDirSync(dir) {
|
|
14205
|
-
if (!
|
|
14206
|
-
const entries =
|
|
15078
|
+
if (!fs8.existsSync(dir)) return;
|
|
15079
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
14207
15080
|
for (const entry of entries) {
|
|
14208
|
-
const p =
|
|
15081
|
+
const p = path16.join(dir, entry.name);
|
|
14209
15082
|
if (entry.isDirectory()) {
|
|
14210
15083
|
rmDirSync(p);
|
|
14211
15084
|
} else {
|
|
14212
|
-
|
|
15085
|
+
fs8.unlinkSync(p);
|
|
14213
15086
|
}
|
|
14214
15087
|
}
|
|
14215
|
-
|
|
15088
|
+
fs8.rmdirSync(dir);
|
|
14216
15089
|
}
|
|
14217
15090
|
var FsPluginStore = class {
|
|
14218
15091
|
getUserPluginsDir() {
|
|
@@ -14220,10 +15093,10 @@ var FsPluginStore = class {
|
|
|
14220
15093
|
}
|
|
14221
15094
|
listUserPluginDirs() {
|
|
14222
15095
|
const dir = userPluginsDir();
|
|
14223
|
-
if (!
|
|
15096
|
+
if (!fs8.existsSync(dir)) return [];
|
|
14224
15097
|
let entries;
|
|
14225
15098
|
try {
|
|
14226
|
-
entries =
|
|
15099
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
14227
15100
|
} catch {
|
|
14228
15101
|
return [];
|
|
14229
15102
|
}
|
|
@@ -14231,7 +15104,7 @@ var FsPluginStore = class {
|
|
|
14231
15104
|
}
|
|
14232
15105
|
listDir(dir) {
|
|
14233
15106
|
try {
|
|
14234
|
-
return
|
|
15107
|
+
return fs8.readdirSync(dir);
|
|
14235
15108
|
} catch {
|
|
14236
15109
|
return [];
|
|
14237
15110
|
}
|
|
@@ -14240,7 +15113,7 @@ var FsPluginStore = class {
|
|
|
14240
15113
|
return loadManifestFromDir(pluginDir);
|
|
14241
15114
|
}
|
|
14242
15115
|
copyDir(sourceDir, destDir) {
|
|
14243
|
-
|
|
15116
|
+
copyDirSync3(sourceDir, destDir);
|
|
14244
15117
|
}
|
|
14245
15118
|
removeDir(dir) {
|
|
14246
15119
|
rmDirSync(dir);
|
|
@@ -14249,20 +15122,20 @@ var FsPluginStore = class {
|
|
|
14249
15122
|
ensureDir(dir);
|
|
14250
15123
|
}
|
|
14251
15124
|
writeFile(filePath, content) {
|
|
14252
|
-
|
|
15125
|
+
fs8.writeFileSync(filePath, content);
|
|
14253
15126
|
}
|
|
14254
15127
|
readFile(filePath) {
|
|
14255
15128
|
try {
|
|
14256
|
-
return
|
|
15129
|
+
return fs8.readFileSync(filePath, "utf8");
|
|
14257
15130
|
} catch {
|
|
14258
15131
|
return null;
|
|
14259
15132
|
}
|
|
14260
15133
|
}
|
|
14261
|
-
dirExists(
|
|
14262
|
-
return
|
|
15134
|
+
dirExists(path20) {
|
|
15135
|
+
return fs8.existsSync(path20) && fs8.statSync(path20).isDirectory();
|
|
14263
15136
|
}
|
|
14264
|
-
exists(
|
|
14265
|
-
return
|
|
15137
|
+
exists(path20) {
|
|
15138
|
+
return fs8.existsSync(path20);
|
|
14266
15139
|
}
|
|
14267
15140
|
};
|
|
14268
15141
|
|
|
@@ -14689,7 +15562,7 @@ var RunnerRegistry = class {
|
|
|
14689
15562
|
return entry?.source === "builtin";
|
|
14690
15563
|
}
|
|
14691
15564
|
installFromPath(sourcePath) {
|
|
14692
|
-
const absPath =
|
|
15565
|
+
const absPath = path17.resolve(sourcePath);
|
|
14693
15566
|
if (!this.store.dirExists(absPath)) {
|
|
14694
15567
|
throw new Error(`Plugin source must be a directory: ${absPath}`);
|
|
14695
15568
|
}
|
|
@@ -14701,7 +15574,7 @@ var RunnerRegistry = class {
|
|
|
14701
15574
|
}
|
|
14702
15575
|
installFromGit(url) {
|
|
14703
15576
|
const validated = assertInstallablePluginUrl(url);
|
|
14704
|
-
const tmpDir =
|
|
15577
|
+
const tmpDir = path17.join(os4.tmpdir(), `ordewell-plugin-${(0, import_crypto3.randomBytes)(12).toString("hex")}`);
|
|
14705
15578
|
this.store.ensureDir(tmpDir);
|
|
14706
15579
|
try {
|
|
14707
15580
|
try {
|
|
@@ -14714,7 +15587,7 @@ var RunnerRegistry = class {
|
|
|
14714
15587
|
const hasManifest = contents.some((f) => f === "manifest.json");
|
|
14715
15588
|
let sourceDir = tmpDir;
|
|
14716
15589
|
if (!hasManifest) {
|
|
14717
|
-
const onlyEntry = contents.length === 1 ?
|
|
15590
|
+
const onlyEntry = contents.length === 1 ? path17.join(tmpDir, contents[0]) : void 0;
|
|
14718
15591
|
if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
|
|
14719
15592
|
throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
|
|
14720
15593
|
}
|
|
@@ -14762,7 +15635,7 @@ var RunnerRegistry = class {
|
|
|
14762
15635
|
}
|
|
14763
15636
|
createSkeleton(name, outputDir) {
|
|
14764
15637
|
assertPlainPluginName(name);
|
|
14765
|
-
const dir =
|
|
15638
|
+
const dir = path17.resolve(outputDir, name);
|
|
14766
15639
|
this.store.ensureDir(dir);
|
|
14767
15640
|
const manifest = {
|
|
14768
15641
|
name,
|
|
@@ -14794,8 +15667,8 @@ var RunnerRegistry = class {
|
|
|
14794
15667
|
]
|
|
14795
15668
|
}
|
|
14796
15669
|
};
|
|
14797
|
-
this.store.writeFile(
|
|
14798
|
-
const runSh =
|
|
15670
|
+
this.store.writeFile(path17.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
15671
|
+
const runSh = path17.join(dir, "run.sh");
|
|
14799
15672
|
const scriptContent = `#!/usr/bin/env bash
|
|
14800
15673
|
# Custom runner script for ${name}
|
|
14801
15674
|
# Receives:
|
|
@@ -14821,30 +15694,30 @@ echo "Running ${name} with prompt: $prompt"
|
|
|
14821
15694
|
};
|
|
14822
15695
|
function fsChmodSync(fpath, mode) {
|
|
14823
15696
|
try {
|
|
14824
|
-
|
|
15697
|
+
fs9.chmodSync(fpath, mode);
|
|
14825
15698
|
} catch {
|
|
14826
15699
|
}
|
|
14827
15700
|
}
|
|
14828
15701
|
|
|
14829
15702
|
// src/utils/stateStore.ts
|
|
14830
|
-
var
|
|
14831
|
-
var
|
|
15703
|
+
var fs10 = __toESM(require("fs"));
|
|
15704
|
+
var path18 = __toESM(require("path"));
|
|
14832
15705
|
var STATE_FILE = "state.json";
|
|
14833
15706
|
function getStatePath(baseDir) {
|
|
14834
|
-
return
|
|
15707
|
+
return path18.join(getStateDir(baseDir), STATE_FILE);
|
|
14835
15708
|
}
|
|
14836
15709
|
function saveState(plan, baseDir) {
|
|
14837
15710
|
const statePath = getStatePath(baseDir ?? process.cwd());
|
|
14838
|
-
ensureDir(
|
|
15711
|
+
ensureDir(path18.dirname(statePath));
|
|
14839
15712
|
plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
14840
|
-
|
|
15713
|
+
fs10.writeFileSync(statePath, JSON.stringify(plan, null, 2));
|
|
14841
15714
|
}
|
|
14842
15715
|
function loadState(baseDir, logger = defaultLogger) {
|
|
14843
15716
|
const dir = baseDir ?? process.cwd();
|
|
14844
15717
|
const statePath = getStatePath(dir);
|
|
14845
|
-
if (!
|
|
15718
|
+
if (!fs10.existsSync(statePath)) return null;
|
|
14846
15719
|
try {
|
|
14847
|
-
const raw =
|
|
15720
|
+
const raw = fs10.readFileSync(statePath, "utf-8");
|
|
14848
15721
|
const parsed = JSON.parse(raw);
|
|
14849
15722
|
if (!Array.isArray(parsed.runners)) {
|
|
14850
15723
|
logger.warn("stateStore", `rejecting state from ${statePath}: old format (scalar "runner" instead of "runners" array). Please re-plan.`);
|
|
@@ -14859,29 +15732,24 @@ function loadState(baseDir, logger = defaultLogger) {
|
|
|
14859
15732
|
function clearState(baseDir) {
|
|
14860
15733
|
const dir = baseDir ?? process.cwd();
|
|
14861
15734
|
const statePath = getStatePath(dir);
|
|
14862
|
-
if (
|
|
15735
|
+
if (fs10.existsSync(statePath)) fs10.unlinkSync(statePath);
|
|
14863
15736
|
}
|
|
14864
15737
|
function stateExists(baseDir) {
|
|
14865
15738
|
const dir = baseDir ?? process.cwd();
|
|
14866
|
-
return
|
|
15739
|
+
return fs10.existsSync(getStatePath(dir));
|
|
14867
15740
|
}
|
|
14868
15741
|
|
|
14869
15742
|
// src/services/SettingsService.ts
|
|
14870
|
-
var
|
|
14871
|
-
var
|
|
14872
|
-
var os6 = __toESM(require("os"));
|
|
15743
|
+
var fs11 = __toESM(require("fs"));
|
|
15744
|
+
var path19 = __toESM(require("path"));
|
|
14873
15745
|
var DEFAULTS = {
|
|
14874
|
-
grillMe: { enabled: false },
|
|
14875
15746
|
tdd: { enabled: true },
|
|
14876
|
-
|
|
14877
|
-
review: { enabled: false },
|
|
14878
|
-
verification: { enabled: false },
|
|
14879
|
-
researchSubagents: { enabled: false }
|
|
15747
|
+
verification: { enabled: false }
|
|
14880
15748
|
};
|
|
14881
15749
|
function getSettingsPath() {
|
|
14882
15750
|
const override = process.env.ORDEWELL_SETTINGS_PATH;
|
|
14883
15751
|
if (override && override.trim()) return override.trim();
|
|
14884
|
-
return
|
|
15752
|
+
return path19.join(globalDataDir(), "settings.json");
|
|
14885
15753
|
}
|
|
14886
15754
|
function readPlannerModels(raw) {
|
|
14887
15755
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
@@ -14914,43 +15782,19 @@ var SettingsService = class {
|
|
|
14914
15782
|
}
|
|
14915
15783
|
fileMtimeMs() {
|
|
14916
15784
|
try {
|
|
14917
|
-
return
|
|
15785
|
+
return fs11.statSync(this.filePath).mtimeMs;
|
|
14918
15786
|
} catch {
|
|
14919
15787
|
return null;
|
|
14920
15788
|
}
|
|
14921
15789
|
}
|
|
14922
|
-
getGrillMe() {
|
|
14923
|
-
return this.getAll().grillMe.enabled;
|
|
14924
|
-
}
|
|
14925
15790
|
getTdd() {
|
|
14926
15791
|
return this.getAll().tdd.enabled;
|
|
14927
15792
|
}
|
|
14928
|
-
getPrd() {
|
|
14929
|
-
return this.getAll().prd.enabled;
|
|
14930
|
-
}
|
|
14931
|
-
setGrillMe(enabled) {
|
|
14932
|
-
this.getAll();
|
|
14933
|
-
this.cache.grillMe.enabled = enabled;
|
|
14934
|
-
this.persist();
|
|
14935
|
-
}
|
|
14936
15793
|
setTdd(enabled) {
|
|
14937
15794
|
this.getAll();
|
|
14938
15795
|
this.cache.tdd.enabled = enabled;
|
|
14939
15796
|
this.persist();
|
|
14940
15797
|
}
|
|
14941
|
-
setPrd(enabled) {
|
|
14942
|
-
this.getAll();
|
|
14943
|
-
this.cache.prd.enabled = enabled;
|
|
14944
|
-
this.persist();
|
|
14945
|
-
}
|
|
14946
|
-
getReview() {
|
|
14947
|
-
return this.getAll().review.enabled;
|
|
14948
|
-
}
|
|
14949
|
-
setReview(enabled) {
|
|
14950
|
-
this.getAll();
|
|
14951
|
-
this.cache.review.enabled = enabled;
|
|
14952
|
-
this.persist();
|
|
14953
|
-
}
|
|
14954
15798
|
getVerification() {
|
|
14955
15799
|
return this.getAll().verification.enabled;
|
|
14956
15800
|
}
|
|
@@ -14959,14 +15803,6 @@ var SettingsService = class {
|
|
|
14959
15803
|
this.cache.verification.enabled = enabled;
|
|
14960
15804
|
this.persist();
|
|
14961
15805
|
}
|
|
14962
|
-
getResearchSubagents() {
|
|
14963
|
-
return this.getAll().researchSubagents.enabled;
|
|
14964
|
-
}
|
|
14965
|
-
setResearchSubagents(enabled) {
|
|
14966
|
-
this.getAll();
|
|
14967
|
-
this.cache.researchSubagents.enabled = enabled;
|
|
14968
|
-
this.persist();
|
|
14969
|
-
}
|
|
14970
15806
|
getModelAllowlist(runner) {
|
|
14971
15807
|
return this.getAll().modelAllowlist?.[runner];
|
|
14972
15808
|
}
|
|
@@ -15016,18 +15852,13 @@ var SettingsService = class {
|
|
|
15016
15852
|
this.persist();
|
|
15017
15853
|
}
|
|
15018
15854
|
load() {
|
|
15855
|
+
migrateOldConfigDir();
|
|
15019
15856
|
try {
|
|
15020
|
-
if (
|
|
15021
|
-
const raw = JSON.parse(
|
|
15857
|
+
if (fs11.existsSync(this.filePath)) {
|
|
15858
|
+
const raw = JSON.parse(fs11.readFileSync(this.filePath, "utf-8"));
|
|
15022
15859
|
const settings = {
|
|
15023
|
-
grillMe: { enabled: raw.grillMe?.enabled ?? DEFAULTS.grillMe.enabled },
|
|
15024
15860
|
tdd: { enabled: raw.tdd?.enabled ?? DEFAULTS.tdd.enabled },
|
|
15025
|
-
|
|
15026
|
-
// `verify` is the legacy key for review mode; keep reading it so old settings files survive the rename.
|
|
15027
|
-
// The evidence-based verification mode uses the distinct `verification` key to avoid colliding with it.
|
|
15028
|
-
review: { enabled: raw.review?.enabled ?? raw.verify?.enabled ?? DEFAULTS.review.enabled },
|
|
15029
|
-
verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled },
|
|
15030
|
-
researchSubagents: { enabled: raw.researchSubagents?.enabled ?? DEFAULTS.researchSubagents.enabled }
|
|
15861
|
+
verification: { enabled: raw.verification?.enabled ?? DEFAULTS.verification.enabled }
|
|
15031
15862
|
};
|
|
15032
15863
|
if (raw.modelAllowlist !== void 0) {
|
|
15033
15864
|
settings.modelAllowlist = raw.modelAllowlist;
|
|
@@ -15043,14 +15874,14 @@ var SettingsService = class {
|
|
|
15043
15874
|
}
|
|
15044
15875
|
} catch {
|
|
15045
15876
|
}
|
|
15046
|
-
return { ...DEFAULTS,
|
|
15877
|
+
return { ...DEFAULTS, tdd: { ...DEFAULTS.tdd }, verification: { ...DEFAULTS.verification } };
|
|
15047
15878
|
}
|
|
15048
15879
|
persist() {
|
|
15049
|
-
const dir =
|
|
15050
|
-
if (!
|
|
15051
|
-
|
|
15880
|
+
const dir = path19.dirname(this.filePath);
|
|
15881
|
+
if (!fs11.existsSync(dir)) {
|
|
15882
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
15052
15883
|
}
|
|
15053
|
-
|
|
15884
|
+
fs11.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2));
|
|
15054
15885
|
this.cachedMtimeMs = this.fileMtimeMs();
|
|
15055
15886
|
}
|
|
15056
15887
|
};
|
|
@@ -15097,6 +15928,7 @@ var PlannerModelMemory = class {
|
|
|
15097
15928
|
AbstractRunner,
|
|
15098
15929
|
AbstractTerminalSession,
|
|
15099
15930
|
ApprovalPolicy,
|
|
15931
|
+
BUILTIN_SKILL_NAMES,
|
|
15100
15932
|
BaseAiService,
|
|
15101
15933
|
BaseConfig,
|
|
15102
15934
|
BaseFileSystem,
|
|
@@ -15155,8 +15987,14 @@ var PlannerModelMemory = class {
|
|
|
15155
15987
|
SYMBOL_LANGUAGES,
|
|
15156
15988
|
Session,
|
|
15157
15989
|
SettingsService,
|
|
15990
|
+
SkillsService,
|
|
15158
15991
|
StdioAgentAdapter,
|
|
15159
15992
|
TASK_OPS_ENVELOPE_KEY,
|
|
15993
|
+
TASK_QUERY_ANSWER_OR_OPS,
|
|
15994
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
15995
|
+
TASK_QUERY_FIELDS,
|
|
15996
|
+
TASK_QUERY_PROTOCOL,
|
|
15997
|
+
TASK_QUERY_REMINDER,
|
|
15160
15998
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
15161
15999
|
TaskOrchestrator,
|
|
15162
16000
|
TmuxRunner,
|
|
@@ -15215,6 +16053,7 @@ var PlannerModelMemory = class {
|
|
|
15215
16053
|
configuredProviders,
|
|
15216
16054
|
createAiService,
|
|
15217
16055
|
createEmptyPlan,
|
|
16056
|
+
createSkillsService,
|
|
15218
16057
|
createTask,
|
|
15219
16058
|
daemonTokenPath,
|
|
15220
16059
|
defaultLogger,
|
|
@@ -15241,12 +16080,14 @@ var PlannerModelMemory = class {
|
|
|
15241
16080
|
filterModelsForPrompt,
|
|
15242
16081
|
filteredBuildModes,
|
|
15243
16082
|
flattenTasks,
|
|
16083
|
+
flattenTasksWithParents,
|
|
15244
16084
|
formatSearchOutput,
|
|
15245
16085
|
generatePlanWithRepair,
|
|
15246
16086
|
getLatestSession,
|
|
15247
16087
|
getProviderMeta,
|
|
15248
16088
|
getSettingsPath,
|
|
15249
16089
|
getStateDir,
|
|
16090
|
+
globalDataDir,
|
|
15250
16091
|
grantScopeFor,
|
|
15251
16092
|
hasTmux,
|
|
15252
16093
|
includeGlobFor,
|
|
@@ -15266,6 +16107,7 @@ var PlannerModelMemory = class {
|
|
|
15266
16107
|
looksLikePlanAttempt,
|
|
15267
16108
|
mapAgentTool,
|
|
15268
16109
|
migrateLegacyPlan,
|
|
16110
|
+
migrateOldConfigDir,
|
|
15269
16111
|
migratePlanState,
|
|
15270
16112
|
migrateTask,
|
|
15271
16113
|
mintDaemonToken,
|
|
@@ -15277,6 +16119,7 @@ var PlannerModelMemory = class {
|
|
|
15277
16119
|
parsePartialPlan,
|
|
15278
16120
|
parsePlanJson,
|
|
15279
16121
|
parseTaskOpsJson,
|
|
16122
|
+
parseTaskQueryJson,
|
|
15280
16123
|
pendingEditRulesBlock,
|
|
15281
16124
|
planDirectLaunch,
|
|
15282
16125
|
planShellLaunch,
|
|
@@ -15285,11 +16128,13 @@ var PlannerModelMemory = class {
|
|
|
15285
16128
|
providerForRunner,
|
|
15286
16129
|
reEmitPlanPrompt,
|
|
15287
16130
|
reEmitTaskOpsPrompt,
|
|
16131
|
+
reEmitTaskQueryPrompt,
|
|
15288
16132
|
readDaemonToken,
|
|
15289
16133
|
referencePattern,
|
|
15290
16134
|
removeTaskFromPlan,
|
|
15291
16135
|
renderPlanMap,
|
|
15292
16136
|
renderPriorOutputs,
|
|
16137
|
+
renderTaskQueryAnswer,
|
|
15293
16138
|
renumberTasks,
|
|
15294
16139
|
repairLoop,
|
|
15295
16140
|
researchShellWarning,
|
|
@@ -15297,10 +16142,12 @@ var PlannerModelMemory = class {
|
|
|
15297
16142
|
resolveArgs,
|
|
15298
16143
|
resolveDefaultMode,
|
|
15299
16144
|
resolveModelShortcut,
|
|
16145
|
+
resolveOrderLabel,
|
|
15300
16146
|
resolvePluginInstallDir,
|
|
15301
16147
|
resolveProvider,
|
|
15302
16148
|
resolveProviderFromPrefix,
|
|
15303
16149
|
resolveResearchShell,
|
|
16150
|
+
resolveSkillInvocation,
|
|
15304
16151
|
resolveTaskMode,
|
|
15305
16152
|
resolveWithin,
|
|
15306
16153
|
retargetTaskRunner,
|
|
@@ -15323,7 +16170,10 @@ var PlannerModelMemory = class {
|
|
|
15323
16170
|
summarizeOutput,
|
|
15324
16171
|
summarizeToolCall,
|
|
15325
16172
|
taskOpsRejectedPrompt,
|
|
16173
|
+
taskOrderLabel,
|
|
16174
|
+
taskQuerySignature,
|
|
15326
16175
|
textHasTaskOps,
|
|
16176
|
+
textHasTaskQuery,
|
|
15327
16177
|
tmuxSessionName,
|
|
15328
16178
|
tmuxSocketName,
|
|
15329
16179
|
tmuxWindowName,
|