taskplane 0.29.2 → 0.30.1
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/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/dashboard/public/app.js +124 -15
- package/dashboard/public/style.css +83 -2
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +78 -63
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +469 -207
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +652 -319
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +832 -280
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +209 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- package/package.json +14 -2
|
@@ -66,7 +66,6 @@ export const CONFIG_VERSION = 1;
|
|
|
66
66
|
*/
|
|
67
67
|
export const PROJECT_CONFIG_FILENAME = "taskplane-config.json";
|
|
68
68
|
|
|
69
|
-
|
|
70
69
|
// ── Task Runner Section Interfaces ───────────────────────────────────
|
|
71
70
|
|
|
72
71
|
/** Project metadata */
|
|
@@ -207,7 +206,6 @@ export interface QualityGateConfig {
|
|
|
207
206
|
passThreshold: PassThreshold;
|
|
208
207
|
}
|
|
209
208
|
|
|
210
|
-
|
|
211
209
|
// ── Task Runner Combined Section ─────────────────────────────────────
|
|
212
210
|
|
|
213
211
|
/**
|
|
@@ -257,7 +255,6 @@ export interface TaskRunnerSection {
|
|
|
257
255
|
modelFallback: ModelFallbackMode;
|
|
258
256
|
}
|
|
259
257
|
|
|
260
|
-
|
|
261
258
|
// ── Orchestrator Section Interfaces ──────────────────────────────────
|
|
262
259
|
|
|
263
260
|
/** Core orchestrator settings */
|
|
@@ -393,7 +390,6 @@ export interface VerificationConfig {
|
|
|
393
390
|
flakyReruns: number;
|
|
394
391
|
}
|
|
395
392
|
|
|
396
|
-
|
|
397
393
|
// ── Orchestrator Combined Section ────────────────────────────────────
|
|
398
394
|
|
|
399
395
|
/**
|
|
@@ -428,7 +424,6 @@ export interface OrchestratorSection {
|
|
|
428
424
|
supervisor: SupervisorSectionConfig;
|
|
429
425
|
}
|
|
430
426
|
|
|
431
|
-
|
|
432
427
|
// ── Workspace Section Interfaces ─────────────────────────────────────
|
|
433
428
|
|
|
434
429
|
/** Workspace repo definition (JSON config shape). */
|
|
@@ -459,7 +454,6 @@ export interface WorkspaceSectionConfig {
|
|
|
459
454
|
routing: WorkspaceRoutingSectionConfig;
|
|
460
455
|
}
|
|
461
456
|
|
|
462
|
-
|
|
463
457
|
// ── Unified Config ───────────────────────────────────────────────────
|
|
464
458
|
|
|
465
459
|
/**
|
|
@@ -491,7 +485,6 @@ export interface TaskplaneConfig {
|
|
|
491
485
|
workspace?: WorkspaceSectionConfig;
|
|
492
486
|
}
|
|
493
487
|
|
|
494
|
-
|
|
495
488
|
// ── Global Preferences (Layer 2) ─────────────────────────────────────
|
|
496
489
|
|
|
497
490
|
/**
|
|
@@ -527,11 +520,12 @@ export interface InitAgentDefaultsPreferences {
|
|
|
527
520
|
mergeThinking?: string;
|
|
528
521
|
}
|
|
529
522
|
|
|
530
|
-
export type DeepPartial<T> =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
523
|
+
export type DeepPartial<T> =
|
|
524
|
+
T extends Array<infer U>
|
|
525
|
+
? Array<DeepPartial<U>>
|
|
526
|
+
: T extends object
|
|
527
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
528
|
+
: T;
|
|
535
529
|
|
|
536
530
|
export interface GlobalPreferences {
|
|
537
531
|
/**
|
|
@@ -590,7 +584,6 @@ export const GLOBAL_PREFERENCES_FILENAME = "preferences.json";
|
|
|
590
584
|
*/
|
|
591
585
|
export const GLOBAL_PREFERENCES_SUBDIR = "taskplane";
|
|
592
586
|
|
|
593
|
-
|
|
594
587
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
595
588
|
|
|
596
589
|
/** Default task runner section values */
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* @module orch/config
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
loadProjectConfig,
|
|
16
|
+
toOrchestratorConfig,
|
|
17
|
+
toTaskRunnerConfig,
|
|
18
|
+
hasConfigFiles,
|
|
19
|
+
} from "./config-loader.ts";
|
|
15
20
|
export { hasConfigFiles, resolveConfigRoot } from "./config-loader.ts";
|
|
16
21
|
import type { OrchestratorConfig, TaskRunnerConfig } from "./types.ts";
|
|
17
22
|
import type { SupervisorConfig } from "./supervisor.ts";
|
|
@@ -31,7 +36,10 @@ import { DEFAULT_SUPERVISOR_CONFIG } from "./supervisor.ts";
|
|
|
31
36
|
*
|
|
32
37
|
* Returns the legacy `OrchestratorConfig` (snake_case) shape.
|
|
33
38
|
*/
|
|
34
|
-
export function loadOrchestratorConfig(
|
|
39
|
+
export function loadOrchestratorConfig(
|
|
40
|
+
cwd: string,
|
|
41
|
+
pointerConfigRoot?: string,
|
|
42
|
+
): OrchestratorConfig {
|
|
35
43
|
const unified = loadProjectConfig(cwd, pointerConfigRoot);
|
|
36
44
|
return toOrchestratorConfig(unified);
|
|
37
45
|
}
|
|
@@ -15,7 +15,15 @@ import { join } from "path";
|
|
|
15
15
|
|
|
16
16
|
import { execLog } from "./execution.ts";
|
|
17
17
|
import { resolveOperatorId } from "./naming.ts";
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AllocatedLane,
|
|
20
|
+
LaneTaskOutcome,
|
|
21
|
+
OrchBatchRuntimeState,
|
|
22
|
+
OrchestratorConfig,
|
|
23
|
+
PersistedTaskRecord,
|
|
24
|
+
BatchDiagnostics,
|
|
25
|
+
PersistedTaskExitSummary,
|
|
26
|
+
} from "./types.ts";
|
|
19
27
|
import { defaultBatchDiagnostics } from "./types.ts";
|
|
20
28
|
|
|
21
29
|
// ── Types ────────────────────────────────────────────────────────────
|
|
@@ -173,7 +181,7 @@ export function buildDiagnosticEvents(input: DiagnosticReportInput): DiagnosticE
|
|
|
173
181
|
* Serialize diagnostic events to JSONL format (one JSON object per line).
|
|
174
182
|
*/
|
|
175
183
|
export function eventsToJsonl(events: DiagnosticEvent[]): string {
|
|
176
|
-
return events.map(e => JSON.stringify(e)).join("\n") + "\n";
|
|
184
|
+
return events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// ── Human-Readable Summary ───────────────────────────────────────────
|
|
@@ -206,7 +214,10 @@ function formatCost(cost: number): string {
|
|
|
206
214
|
/**
|
|
207
215
|
* Generate a human-readable markdown summary report.
|
|
208
216
|
*/
|
|
209
|
-
export function buildMarkdownReport(
|
|
217
|
+
export function buildMarkdownReport(
|
|
218
|
+
input: DiagnosticReportInput,
|
|
219
|
+
events: DiagnosticEvent[],
|
|
220
|
+
): string {
|
|
210
221
|
const { batchId, phase, mode, startedAt, endedAt, diagnostics } = input;
|
|
211
222
|
const { succeededTasks, failedTasks, skippedTasks, blockedTasks, totalTasks } = input;
|
|
212
223
|
|
|
@@ -248,7 +259,7 @@ export function buildMarkdownReport(input: DiagnosticReportInput, events: Diagno
|
|
|
248
259
|
lines.push(`|------|--------|---------------|------|----------|---------|`);
|
|
249
260
|
for (const evt of events) {
|
|
250
261
|
lines.push(
|
|
251
|
-
`| ${evt.taskId} | ${evt.status} | ${evt.classification} | ${formatCost(evt.cost)} | ${formatDuration(evt.durationSec)} | ${evt.retries}
|
|
262
|
+
`| ${evt.taskId} | ${evt.status} | ${evt.classification} | ${formatCost(evt.cost)} | ${formatDuration(evt.durationSec)} | ${evt.retries} |`,
|
|
252
263
|
);
|
|
253
264
|
}
|
|
254
265
|
lines.push(``);
|
|
@@ -276,8 +287,8 @@ export function buildMarkdownReport(input: DiagnosticReportInput, events: Diagno
|
|
|
276
287
|
} else {
|
|
277
288
|
for (const repoKey of repoKeys) {
|
|
278
289
|
const repoEvents = byRepo.get(repoKey)!;
|
|
279
|
-
const repoSucceeded = repoEvents.filter(e => e.status === "succeeded").length;
|
|
280
|
-
const repoFailed = repoEvents.filter(e => e.status === "failed").length;
|
|
290
|
+
const repoSucceeded = repoEvents.filter((e) => e.status === "succeeded").length;
|
|
291
|
+
const repoFailed = repoEvents.filter((e) => e.status === "failed").length;
|
|
281
292
|
const repoCost = repoEvents.reduce((sum, e) => sum + e.cost, 0);
|
|
282
293
|
|
|
283
294
|
lines.push(`### ${repoKey}`);
|
|
@@ -290,7 +301,7 @@ export function buildMarkdownReport(input: DiagnosticReportInput, events: Diagno
|
|
|
290
301
|
lines.push(`|------|--------|---------------|------|----------|`);
|
|
291
302
|
for (const evt of repoEvents) {
|
|
292
303
|
lines.push(
|
|
293
|
-
`| ${evt.taskId} | ${evt.status} | ${evt.classification} | ${formatCost(evt.cost)} | ${formatDuration(evt.durationSec)}
|
|
304
|
+
`| ${evt.taskId} | ${evt.status} | ${evt.classification} | ${formatCost(evt.cost)} | ${formatDuration(evt.durationSec)} |`,
|
|
294
305
|
);
|
|
295
306
|
}
|
|
296
307
|
lines.push(``);
|
|
@@ -377,7 +388,10 @@ export function assembleDiagnosticInput(
|
|
|
377
388
|
): DiagnosticReportInput {
|
|
378
389
|
// Build lookup maps for fast per-task enrichment (mirrors serializeBatchState logic).
|
|
379
390
|
const laneByTaskId = new Map<string, AllocatedLane>();
|
|
380
|
-
const allocatedTaskByTaskId = new Map<
|
|
391
|
+
const allocatedTaskByTaskId = new Map<
|
|
392
|
+
string,
|
|
393
|
+
{ allocatedTask: import("./types.ts").AllocatedTask; lane: AllocatedLane }
|
|
394
|
+
>();
|
|
381
395
|
for (const lane of lanes) {
|
|
382
396
|
for (const allocTask of lane.tasks) {
|
|
383
397
|
laneByTaskId.set(allocTask.taskId, lane);
|
|
@@ -401,48 +415,46 @@ export function assembleDiagnosticInput(
|
|
|
401
415
|
}
|
|
402
416
|
|
|
403
417
|
// Build task records sorted by taskId for deterministic output.
|
|
404
|
-
const tasks: PersistedTaskRecord[] = [...taskIdSet]
|
|
405
|
-
.
|
|
406
|
-
.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
exitReason: outcome?.exitReason ?? "",
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
// Repo attribution from allocated task metadata (workspace mode).
|
|
424
|
-
if (allocated?.allocatedTask.task?.promptRepoId !== undefined) {
|
|
425
|
-
record.repoId = allocated.allocatedTask.task.promptRepoId;
|
|
426
|
-
}
|
|
427
|
-
if (allocated?.allocatedTask.task?.resolvedRepoId !== undefined) {
|
|
428
|
-
record.resolvedRepoId = allocated.allocatedTask.task.resolvedRepoId;
|
|
429
|
-
}
|
|
418
|
+
const tasks: PersistedTaskRecord[] = [...taskIdSet].sort().map((taskId): PersistedTaskRecord => {
|
|
419
|
+
const lane = laneByTaskId.get(taskId);
|
|
420
|
+
const outcome = outcomeByTaskId.get(taskId);
|
|
421
|
+
const allocated = allocatedTaskByTaskId.get(taskId);
|
|
422
|
+
|
|
423
|
+
const record: PersistedTaskRecord = {
|
|
424
|
+
taskId,
|
|
425
|
+
laneNumber: lane?.laneNumber ?? 0,
|
|
426
|
+
sessionName: outcome?.sessionName || lane?.laneSessionId || "",
|
|
427
|
+
status: outcome?.status ?? "pending",
|
|
428
|
+
taskFolder: "",
|
|
429
|
+
startedAt: outcome?.startTime ?? null,
|
|
430
|
+
endedAt: outcome?.endTime ?? null,
|
|
431
|
+
doneFileFound: outcome?.doneFileFound ?? false,
|
|
432
|
+
exitReason: outcome?.exitReason ?? "",
|
|
433
|
+
};
|
|
430
434
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
// Repo attribution from allocated task metadata (workspace mode).
|
|
436
|
+
if (allocated?.allocatedTask.task?.promptRepoId !== undefined) {
|
|
437
|
+
record.repoId = allocated.allocatedTask.task.promptRepoId;
|
|
438
|
+
}
|
|
439
|
+
if (allocated?.allocatedTask.task?.resolvedRepoId !== undefined) {
|
|
440
|
+
record.resolvedRepoId = allocated.allocatedTask.task.resolvedRepoId;
|
|
441
|
+
}
|
|
438
442
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
+
// Partial progress fields from outcome.
|
|
444
|
+
if (outcome?.partialProgressCommits !== undefined) {
|
|
445
|
+
record.partialProgressCommits = outcome.partialProgressCommits;
|
|
446
|
+
}
|
|
447
|
+
if (outcome?.partialProgressBranch !== undefined) {
|
|
448
|
+
record.partialProgressBranch = outcome.partialProgressBranch;
|
|
449
|
+
}
|
|
443
450
|
|
|
444
|
-
|
|
445
|
-
|
|
451
|
+
// v3: Exit diagnostic from outcome.
|
|
452
|
+
if (outcome?.exitDiagnostic !== undefined) {
|
|
453
|
+
record.exitDiagnostic = outcome.exitDiagnostic;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return record;
|
|
457
|
+
});
|
|
446
458
|
|
|
447
459
|
return {
|
|
448
460
|
orchConfig,
|
|
@@ -249,20 +249,20 @@ export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
|
|
|
249
249
|
* @since TP-055
|
|
250
250
|
*/
|
|
251
251
|
export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
|
|
252
|
-
/\b(?:401|403)\b/,
|
|
253
|
-
/\b429\b/,
|
|
254
|
-
/model[_ ]not[_ ]found/i,
|
|
255
|
-
/model[_ ](?:is[_ ])?unavailable/i,
|
|
256
|
-
/model[_ ](?:has[_ ]been[_ ])?deprecated/i,
|
|
252
|
+
/\b(?:401|403)\b/, // HTTP auth/forbidden status codes
|
|
253
|
+
/\b429\b/, // HTTP rate limit
|
|
254
|
+
/model[_ ]not[_ ]found/i, // Model not found
|
|
255
|
+
/model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
|
|
256
|
+
/model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
|
|
257
257
|
/api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
|
|
258
|
-
/invalid[_ ]api[_ ]key/i,
|
|
258
|
+
/invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
|
|
259
259
|
/authentication[_ ](?:failed|error|required)/i, // Auth failures
|
|
260
|
-
/authorization[_ ](?:failed|error|denied)/i,
|
|
261
|
-
/access[_ ]denied/i,
|
|
262
|
-
/permission[_ ]denied/i,
|
|
263
|
-
/quota[_ ]exceeded/i,
|
|
264
|
-
/rate[_ ]limit/i,
|
|
265
|
-
/insufficient[_ ]quota/i,
|
|
260
|
+
/authorization[_ ](?:failed|error|denied)/i, // Authz failures
|
|
261
|
+
/access[_ ]denied/i, // Generic access denied
|
|
262
|
+
/permission[_ ]denied/i, // Permission denied
|
|
263
|
+
/quota[_ ]exceeded/i, // Quota exceeded
|
|
264
|
+
/rate[_ ]limit/i, // Rate limit (phrase)
|
|
265
|
+
/insufficient[_ ]quota/i, // Insufficient quota
|
|
266
266
|
];
|
|
267
267
|
|
|
268
268
|
/**
|
|
@@ -277,7 +277,7 @@ export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
|
|
|
277
277
|
*/
|
|
278
278
|
export function isModelAccessError(errorMessage: string): boolean {
|
|
279
279
|
if (!errorMessage) return false;
|
|
280
|
-
return MODEL_ACCESS_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
|
|
280
|
+
return MODEL_ACCESS_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
@@ -6,7 +6,16 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "
|
|
|
6
6
|
import { join, dirname, basename, resolve } from "path";
|
|
7
7
|
|
|
8
8
|
import { FATAL_DISCOVERY_CODES } from "./types.ts";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
DiscoveryError,
|
|
11
|
+
DiscoveryResult,
|
|
12
|
+
ParsedTask,
|
|
13
|
+
PromptSegmentDagMetadata,
|
|
14
|
+
SegmentCheckboxGroup,
|
|
15
|
+
StepSegmentMapping,
|
|
16
|
+
TaskArea,
|
|
17
|
+
WorkspaceConfig,
|
|
18
|
+
} from "./types.ts";
|
|
10
19
|
|
|
11
20
|
// ── PROMPT.md Parsing ────────────────────────────────────────────────
|
|
12
21
|
|
|
@@ -233,8 +242,7 @@ function parseSegmentDagMetadata(
|
|
|
233
242
|
metadata: null,
|
|
234
243
|
error: {
|
|
235
244
|
code: "SEGMENT_DAG_INVALID",
|
|
236
|
-
message:
|
|
237
|
-
`Task ${taskId} has self-edge "${fromRepoId} -> ${toRepoId}" in ## Segment DAG at line ${baseLine + i}.`,
|
|
245
|
+
message: `Task ${taskId} has self-edge "${fromRepoId} -> ${toRepoId}" in ## Segment DAG at line ${baseLine + i}.`,
|
|
238
246
|
taskId,
|
|
239
247
|
taskPath: promptPath,
|
|
240
248
|
},
|
|
@@ -258,8 +266,7 @@ function parseSegmentDagMetadata(
|
|
|
258
266
|
metadata: null,
|
|
259
267
|
error: {
|
|
260
268
|
code: "SEGMENT_REPO_UNKNOWN",
|
|
261
|
-
message:
|
|
262
|
-
`Task ${taskId} has edge endpoint repo "${edge.fromRepoId}" in ## Segment DAG that is not declared in Repos:.`,
|
|
269
|
+
message: `Task ${taskId} has edge endpoint repo "${edge.fromRepoId}" in ## Segment DAG that is not declared in Repos:.`,
|
|
263
270
|
taskId,
|
|
264
271
|
taskPath: promptPath,
|
|
265
272
|
},
|
|
@@ -270,8 +277,7 @@ function parseSegmentDagMetadata(
|
|
|
270
277
|
metadata: null,
|
|
271
278
|
error: {
|
|
272
279
|
code: "SEGMENT_REPO_UNKNOWN",
|
|
273
|
-
message:
|
|
274
|
-
`Task ${taskId} has edge endpoint repo "${edge.toRepoId}" in ## Segment DAG that is not declared in Repos:.`,
|
|
280
|
+
message: `Task ${taskId} has edge endpoint repo "${edge.toRepoId}" in ## Segment DAG that is not declared in Repos:.`,
|
|
275
281
|
taskId,
|
|
276
282
|
taskPath: promptPath,
|
|
277
283
|
},
|
|
@@ -334,8 +340,7 @@ function parseSegmentDagMetadata(
|
|
|
334
340
|
metadata: null,
|
|
335
341
|
error: {
|
|
336
342
|
code: "SEGMENT_DAG_INVALID",
|
|
337
|
-
message:
|
|
338
|
-
`Task ${taskId} has cyclic ## Segment DAG metadata: ${cycle.join(" -> ")}.`,
|
|
343
|
+
message: `Task ${taskId} has cyclic ## Segment DAG metadata: ${cycle.join(" -> ")}.`,
|
|
339
344
|
taskId,
|
|
340
345
|
taskPath: promptPath,
|
|
341
346
|
},
|
|
@@ -516,15 +521,15 @@ export function parseStepSegmentMapping(
|
|
|
516
521
|
}
|
|
517
522
|
seenRepoIds.add(seg.repoId);
|
|
518
523
|
|
|
519
|
-
const nextSegIndex =
|
|
524
|
+
const nextSegIndex =
|
|
525
|
+
j + 1 < segmentHeaders.length ? segmentHeaders[j + 1].index : stepContent.length;
|
|
520
526
|
const segContent = stepContent.slice(seg.index, nextSegIndex);
|
|
521
527
|
const checkboxes = extractCheckboxes(segContent);
|
|
522
528
|
|
|
523
529
|
if (checkboxes.length === 0) {
|
|
524
530
|
warnings.push({
|
|
525
531
|
code: "SEGMENT_STEP_EMPTY",
|
|
526
|
-
message:
|
|
527
|
-
`Task ${taskId} Step ${header.stepNumber} has empty segment "${seg.repoId}" with no checkboxes.`,
|
|
532
|
+
message: `Task ${taskId} Step ${header.stepNumber} has empty segment "${seg.repoId}" with no checkboxes.`,
|
|
528
533
|
taskId,
|
|
529
534
|
});
|
|
530
535
|
}
|
|
@@ -650,9 +655,7 @@ export function parsePromptForOrchestrator(
|
|
|
650
655
|
|
|
651
656
|
// ── Extract dependencies ─────────────────────────────────────
|
|
652
657
|
const dependencies: string[] = [];
|
|
653
|
-
const depSectionMatch = content.match(
|
|
654
|
-
/^##\s+Dependencies\s*\n([\s\S]*?)(?=\n##\s|\n---|\n$)/m,
|
|
655
|
-
);
|
|
658
|
+
const depSectionMatch = content.match(/^##\s+Dependencies\s*\n([\s\S]*?)(?=\n##\s|\n---|\n$)/m);
|
|
656
659
|
|
|
657
660
|
if (depSectionMatch) {
|
|
658
661
|
const depBody = depSectionMatch[1].trim();
|
|
@@ -669,9 +672,7 @@ export function parsePromptForOrchestrator(
|
|
|
669
672
|
}
|
|
670
673
|
|
|
671
674
|
// Pattern 2: Bullet list "- COMP-005 ...", "- **time-off/TO-014** ..."
|
|
672
|
-
const bulletMatches = depBody.matchAll(
|
|
673
|
-
/^[\s-]*\*?\*?((?:[a-z0-9-]+\/)?[A-Z]+-\d+)\*?\*?/gim,
|
|
674
|
-
);
|
|
675
|
+
const bulletMatches = depBody.matchAll(/^[\s-]*\*?\*?((?:[a-z0-9-]+\/)?[A-Z]+-\d+)\*?\*?/gim);
|
|
675
676
|
for (const m of bulletMatches) {
|
|
676
677
|
const dep = normalizeDependencyReference(m[1]);
|
|
677
678
|
if (!dependencies.includes(dep)) dependencies.push(dep);
|
|
@@ -709,15 +710,13 @@ export function parsePromptForOrchestrator(
|
|
|
709
710
|
if (afterHeader !== -1) {
|
|
710
711
|
const rest = content.slice(afterHeader + 1);
|
|
711
712
|
const nextSectionMatch = rest.search(/^##\s|^---/m);
|
|
712
|
-
execTargetSectionBody = nextSectionMatch !== -1
|
|
713
|
-
? rest.slice(0, nextSectionMatch)
|
|
714
|
-
: rest;
|
|
713
|
+
execTargetSectionBody = nextSectionMatch !== -1 ? rest.slice(0, nextSectionMatch) : rest;
|
|
715
714
|
}
|
|
716
715
|
}
|
|
717
716
|
if (execTargetSectionBody !== null) {
|
|
718
717
|
// Match "Repo: api" or "**Repo:** api" or "Workspace: api" with whitespace
|
|
719
718
|
const repoLineMatch = execTargetSectionBody.match(
|
|
720
|
-
/^\s*\*?\*?(?:Repo|Workspace):?\*?\*?\s+(\S+)/
|
|
719
|
+
/^\s*\*?\*?(?:Repo|Workspace):?\*?\*?\s+(\S+)/im,
|
|
721
720
|
);
|
|
722
721
|
if (repoLineMatch) {
|
|
723
722
|
const candidate = repoLineMatch[1].trim().toLowerCase();
|
|
@@ -729,9 +728,7 @@ export function parsePromptForOrchestrator(
|
|
|
729
728
|
|
|
730
729
|
// Priority 2 (fallback): Inline "**Repo:** <id>" or "**Workspace:** <id>" anywhere in content
|
|
731
730
|
if (!promptRepoId) {
|
|
732
|
-
const inlineRepoMatch = content.match(
|
|
733
|
-
/^\*\*(?:Repo|Workspace):\*\*\s+(\S+)/m,
|
|
734
|
-
);
|
|
731
|
+
const inlineRepoMatch = content.match(/^\*\*(?:Repo|Workspace):\*\*\s+(\S+)/m);
|
|
735
732
|
if (inlineRepoMatch) {
|
|
736
733
|
const candidate = inlineRepoMatch[1].trim().toLowerCase();
|
|
737
734
|
if (REPO_ID_PATTERN.test(candidate)) {
|
|
@@ -742,9 +739,7 @@ export function parsePromptForOrchestrator(
|
|
|
742
739
|
|
|
743
740
|
// ── Extract file scope ───────────────────────────────────────
|
|
744
741
|
const fileScope: string[] = [];
|
|
745
|
-
const fileScopeMatch = content.match(
|
|
746
|
-
/^##\s+File Scope\s*\n([\s\S]*?)(?=\n##\s|\n---|\n$)/m,
|
|
747
|
-
);
|
|
742
|
+
const fileScopeMatch = content.match(/^##\s+File Scope\s*\n([\s\S]*?)(?=\n##\s|\n---|\n$)/m);
|
|
748
743
|
|
|
749
744
|
if (fileScopeMatch) {
|
|
750
745
|
const scopeBody = fileScopeMatch[1].trim();
|
|
@@ -812,9 +807,43 @@ export function parsePromptForOrchestrator(
|
|
|
812
807
|
};
|
|
813
808
|
}
|
|
814
809
|
|
|
815
|
-
|
|
816
810
|
// ── Area Scanning ────────────────────────────────────────────────────
|
|
817
811
|
|
|
812
|
+
/**
|
|
813
|
+
* TP-196 / #462 — Discovery safeguard for `.DONE` authority drift.
|
|
814
|
+
*
|
|
815
|
+
* Discovery has no access to persisted segment state, so it cannot make a
|
|
816
|
+
* hard `.DONE` vs. segment-frontier authority decision (that lives in the
|
|
817
|
+
* monitor/resume guards). What it CAN do cheaply is detect the most common
|
|
818
|
+
* symptom of a stale or premature `.DONE`: a `.DONE` file exists alongside
|
|
819
|
+
* a STATUS.md that still has unchecked checkboxes. When that pattern is
|
|
820
|
+
* found, emit a one-line `console.warn` so operators see the inconsistency
|
|
821
|
+
* during scan. Behaviour of `scanAreaForTasks` is unchanged — the task is
|
|
822
|
+
* still skipped — this is a doctor-style warning only.
|
|
823
|
+
*
|
|
824
|
+
* Returns `true` when the safeguard issued a warning (used by tests).
|
|
825
|
+
*/
|
|
826
|
+
export function checkDoneAuthoritySafeguard(
|
|
827
|
+
taskFolder: string,
|
|
828
|
+
logger: (msg: string) => void = console.warn,
|
|
829
|
+
): boolean {
|
|
830
|
+
const statusPath = join(taskFolder, "STATUS.md");
|
|
831
|
+
if (!existsSync(statusPath)) return false;
|
|
832
|
+
let content: string;
|
|
833
|
+
try {
|
|
834
|
+
content = readFileSync(statusPath, "utf-8");
|
|
835
|
+
} catch {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
// Look for any unchecked checkbox `- [ ]` on its own line.
|
|
839
|
+
const hasUnchecked = /^\s*-\s*\[\s\]\s+/m.test(content);
|
|
840
|
+
if (!hasUnchecked) return false;
|
|
841
|
+
logger(
|
|
842
|
+
`[discovery] WARN: .DONE present in ${taskFolder} but STATUS.md contains unchecked checkboxes — possible stale/premature .DONE (#462 safeguard).`,
|
|
843
|
+
);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
|
|
818
847
|
/**
|
|
819
848
|
* Scan an area path for pending tasks.
|
|
820
849
|
*
|
|
@@ -864,8 +893,14 @@ export function scanAreaForTasks(
|
|
|
864
893
|
continue;
|
|
865
894
|
}
|
|
866
895
|
|
|
867
|
-
// Skip if .DONE exists (already complete)
|
|
868
|
-
if
|
|
896
|
+
// Skip if .DONE exists (already complete).
|
|
897
|
+
// TP-196 / #462: doctor-style safeguard — if .DONE coexists with
|
|
898
|
+
// unchecked checkboxes in STATUS.md, warn so operators can investigate
|
|
899
|
+
// before the task is silently treated as complete.
|
|
900
|
+
if (existsSync(join(entryPath, ".DONE"))) {
|
|
901
|
+
checkDoneAuthoritySafeguard(entryPath);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
869
904
|
|
|
870
905
|
// Skip if no PROMPT.md
|
|
871
906
|
const promptPath = join(entryPath, "PROMPT.md");
|
|
@@ -887,7 +922,6 @@ export function scanAreaForTasks(
|
|
|
887
922
|
return { tasks, errors };
|
|
888
923
|
}
|
|
889
924
|
|
|
890
|
-
|
|
891
925
|
// ── Completed Task Set ───────────────────────────────────────────────
|
|
892
926
|
|
|
893
927
|
/**
|
|
@@ -957,7 +991,6 @@ export function buildCompletedTaskSet(areaPaths: string[]): Set<string> {
|
|
|
957
991
|
return completed;
|
|
958
992
|
}
|
|
959
993
|
|
|
960
|
-
|
|
961
994
|
// ── Argument Resolution ──────────────────────────────────────────────
|
|
962
995
|
|
|
963
996
|
/**
|
|
@@ -995,10 +1028,7 @@ export function resolveArguments(
|
|
|
995
1028
|
if (!areaScanPaths.includes(fullPath)) {
|
|
996
1029
|
areaScanPaths.push(fullPath);
|
|
997
1030
|
}
|
|
998
|
-
} else if (
|
|
999
|
-
token.endsWith("PROMPT.md") &&
|
|
1000
|
-
existsSync(resolve(cwd, token))
|
|
1001
|
-
) {
|
|
1031
|
+
} else if (token.endsWith("PROMPT.md") && existsSync(resolve(cwd, token))) {
|
|
1002
1032
|
// Single PROMPT.md file
|
|
1003
1033
|
directTaskFolders.push(resolve(cwd, dirname(token)));
|
|
1004
1034
|
} else if (existsSync(resolve(cwd, token))) {
|
|
@@ -1129,7 +1159,6 @@ export function applyDependenciesFromCache(
|
|
|
1129
1159
|
return { applied };
|
|
1130
1160
|
}
|
|
1131
1161
|
|
|
1132
|
-
|
|
1133
1162
|
// ── Task Registry ────────────────────────────────────────────────────
|
|
1134
1163
|
|
|
1135
1164
|
/**
|
|
@@ -1238,7 +1267,6 @@ export function buildTaskRegistry(
|
|
|
1238
1267
|
return { pending, completed, errors };
|
|
1239
1268
|
}
|
|
1240
1269
|
|
|
1241
|
-
|
|
1242
1270
|
// ── Cross-Area Dependency Resolution ─────────────────────────────────
|
|
1243
1271
|
|
|
1244
1272
|
/** Candidate match for a dependency reference found in task areas. */
|
|
@@ -1407,7 +1435,6 @@ export function resolveDependencies(
|
|
|
1407
1435
|
return errors;
|
|
1408
1436
|
}
|
|
1409
1437
|
|
|
1410
|
-
|
|
1411
1438
|
// ── Task-to-Repo Routing ─────────────────────────────────────────────
|
|
1412
1439
|
|
|
1413
1440
|
/** Repo ID validation: lowercase alphanumeric + hyphens, starting with alnum */
|
|
@@ -1440,7 +1467,9 @@ export function resolveTaskRouting(
|
|
|
1440
1467
|
for (const task of discovery.pending.values()) {
|
|
1441
1468
|
// ── Explicit segment DAG repo validation (workspace IDs) ─
|
|
1442
1469
|
if (task.explicitSegmentDag) {
|
|
1443
|
-
const unknownRepos = task.explicitSegmentDag.repoIds.filter(
|
|
1470
|
+
const unknownRepos = task.explicitSegmentDag.repoIds.filter(
|
|
1471
|
+
(repoId) => !validRepoIds.has(repoId),
|
|
1472
|
+
);
|
|
1444
1473
|
if (unknownRepos.length > 0) {
|
|
1445
1474
|
errors.push({
|
|
1446
1475
|
code: "SEGMENT_REPO_UNKNOWN",
|
|
@@ -1577,9 +1606,8 @@ export function resolveTaskRouting(
|
|
|
1577
1606
|
if (!validRepoIds.has(seg.repoId)) {
|
|
1578
1607
|
const knownRepos = [...validRepoIds.keys()];
|
|
1579
1608
|
const suggestions = suggestRepoMatches(seg.repoId, knownRepos);
|
|
1580
|
-
const suggestionHint =
|
|
1581
|
-
? ` Did you mean: ${suggestions.join(", ")}?`
|
|
1582
|
-
: "";
|
|
1609
|
+
const suggestionHint =
|
|
1610
|
+
suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
1583
1611
|
errors.push({
|
|
1584
1612
|
code: "SEGMENT_STEP_REPO_INVALID",
|
|
1585
1613
|
message:
|
|
@@ -1599,7 +1627,6 @@ export function resolveTaskRouting(
|
|
|
1599
1627
|
return errors;
|
|
1600
1628
|
}
|
|
1601
1629
|
|
|
1602
|
-
|
|
1603
1630
|
// ── Discovery Pipeline (Public) ──────────────────────────────────────
|
|
1604
1631
|
|
|
1605
1632
|
/**
|
|
@@ -1721,7 +1748,7 @@ export function runDiscovery(
|
|
|
1721
1748
|
for (const task of discovery.pending.values()) {
|
|
1722
1749
|
if (!task.stepSegmentMap) continue;
|
|
1723
1750
|
for (const step of task.stepSegmentMap) {
|
|
1724
|
-
const stepRepoIds = step.segments.map(s => s.repoId);
|
|
1751
|
+
const stepRepoIds = step.segments.map((s) => s.repoId);
|
|
1725
1752
|
const seen = new Set<string>();
|
|
1726
1753
|
for (const rid of stepRepoIds) {
|
|
1727
1754
|
if (seen.has(rid)) {
|
|
@@ -1765,26 +1792,15 @@ export function formatDiscoveryResults(result: DiscoveryResult): string {
|
|
|
1765
1792
|
}
|
|
1766
1793
|
|
|
1767
1794
|
lines.push("Pending Tasks:");
|
|
1768
|
-
const sortedAreas = [...byArea.entries()].sort((a, b) =>
|
|
1769
|
-
a[0].localeCompare(b[0]),
|
|
1770
|
-
);
|
|
1795
|
+
const sortedAreas = [...byArea.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1771
1796
|
for (const [area, tasks] of sortedAreas) {
|
|
1772
1797
|
lines.push(` ${area}:`);
|
|
1773
|
-
const sortedTasks = [...tasks].sort((a, b) =>
|
|
1774
|
-
a.taskId.localeCompare(b.taskId),
|
|
1775
|
-
);
|
|
1798
|
+
const sortedTasks = [...tasks].sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
1776
1799
|
for (const task of sortedTasks) {
|
|
1777
1800
|
const deps =
|
|
1778
|
-
task.dependencies.length > 0
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const repo =
|
|
1782
|
-
task.resolvedRepoId
|
|
1783
|
-
? ` → repo: ${task.resolvedRepoId}`
|
|
1784
|
-
: "";
|
|
1785
|
-
lines.push(
|
|
1786
|
-
` ${task.taskId} [${task.size}] ${task.taskName}${deps}${repo}`,
|
|
1787
|
-
);
|
|
1801
|
+
task.dependencies.length > 0 ? ` → depends on: ${task.dependencies.join(", ")}` : "";
|
|
1802
|
+
const repo = task.resolvedRepoId ? ` → repo: ${task.resolvedRepoId}` : "";
|
|
1803
|
+
lines.push(` ${task.taskId} [${task.size}] ${task.taskName}${deps}${repo}`);
|
|
1788
1804
|
}
|
|
1789
1805
|
}
|
|
1790
1806
|
lines.push("");
|
|
@@ -1815,4 +1831,3 @@ export function formatDiscoveryResults(result: DiscoveryResult): string {
|
|
|
1815
1831
|
|
|
1816
1832
|
return lines.join("\n");
|
|
1817
1833
|
}
|
|
1818
|
-
|