taskplane 0.22.3 → 0.22.5
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/dashboard/server.cjs +11 -1
- package/extensions/task-runner.ts +30 -2
- package/extensions/taskplane/config-loader.ts +109 -1
- package/extensions/taskplane/config-schema.ts +33 -0
- package/extensions/taskplane/execution.ts +5 -1
- package/extensions/taskplane/extension.ts +33 -19
- package/extensions/taskplane/types.ts +22 -6
- package/extensions/taskplane/workspace.ts +115 -3
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +24 -0
- package/templates/agents/task-worker.md +9 -0
package/dashboard/server.cjs
CHANGED
|
@@ -792,7 +792,17 @@ function computeBatchTotalCost(laneStates, telemetry) {
|
|
|
792
792
|
function buildDashboardState() {
|
|
793
793
|
const state = loadBatchState();
|
|
794
794
|
const tmuxSessions = getTmuxSessions();
|
|
795
|
-
const
|
|
795
|
+
const rawLaneStates = loadLaneStates();
|
|
796
|
+
// Filter stale lane states from previous batches.
|
|
797
|
+
// Lane state files persist across batches (same filename), so without
|
|
798
|
+
// filtering the dashboard shows telemetry from old runs.
|
|
799
|
+
const currentBatchId = state?.batchId || null;
|
|
800
|
+
const laneStates = {};
|
|
801
|
+
for (const [key, ls] of Object.entries(rawLaneStates)) {
|
|
802
|
+
if (!currentBatchId || !ls.batchId || ls.batchId === currentBatchId) {
|
|
803
|
+
laneStates[key] = ls;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
796
806
|
const telemetry = loadTelemetryData(state);
|
|
797
807
|
const batchTotalCost = computeBatchTotalCost(laneStates, telemetry);
|
|
798
808
|
const supervisor = loadSupervisorData(state);
|
|
@@ -447,6 +447,7 @@ function writeLaneState(state: TaskState): void {
|
|
|
447
447
|
reviewerOutputTokens: state.reviewerOutputTokens || 0,
|
|
448
448
|
reviewerCacheReadTokens: state.reviewerCacheReadTokens || 0,
|
|
449
449
|
reviewerCacheWriteTokens: state.reviewerCacheWriteTokens || 0,
|
|
450
|
+
batchId: process.env.ORCH_BATCH_ID || null,
|
|
450
451
|
timestamp: Date.now(),
|
|
451
452
|
};
|
|
452
453
|
writeFileSync(filePath, JSON.stringify(data) + "\n");
|
|
@@ -2973,8 +2974,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
2973
2974
|
return;
|
|
2974
2975
|
}
|
|
2975
2976
|
} else {
|
|
2976
|
-
// ──
|
|
2977
|
-
//
|
|
2977
|
+
// ── Empty completion guard ────────────────────────────────
|
|
2978
|
+
// Detect tasks where the worker checked off STATUS.md without
|
|
2979
|
+
// modifying any source files. This catches "shortcut" completions
|
|
2980
|
+
// where the worker concludes work is "already done" without
|
|
2981
|
+
// implementing anything.
|
|
2982
|
+
if (isOrchestratedMode()) {
|
|
2983
|
+
try {
|
|
2984
|
+
const diffResult = spawnSync("git", ["diff", "--name-only", "HEAD"], {
|
|
2985
|
+
cwd: task.taskFolder, encoding: "utf-8", timeout: 10_000,
|
|
2986
|
+
});
|
|
2987
|
+
const changedFiles = (diffResult.stdout || "").split("\n").filter(Boolean);
|
|
2988
|
+
const sourceChanges = changedFiles.filter(f =>
|
|
2989
|
+
!f.endsWith("STATUS.md") && !f.endsWith(".DONE") &&
|
|
2990
|
+
!f.includes(".reviews/") && !f.endsWith("dependencies.json")
|
|
2991
|
+
);
|
|
2992
|
+
if (sourceChanges.length === 0) {
|
|
2993
|
+
logExecution(statusPath, "⚠️ Empty completion",
|
|
2994
|
+
"Worker marked all steps complete but no source files were modified. " +
|
|
2995
|
+
"Only STATUS.md changes detected. This may indicate the worker shortcut " +
|
|
2996
|
+
"the task without implementing. .DONE will still be created, but this " +
|
|
2997
|
+
"should be investigated.");
|
|
2998
|
+
console.error(`[task-runner] WARNING: Task ${task.taskId} completed with zero source file changes`);
|
|
2999
|
+
}
|
|
3000
|
+
} catch {
|
|
3001
|
+
// Best effort — don't block .DONE creation on git check failure
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
// Create .DONE
|
|
2978
3006
|
const donePath = join(task.taskFolder, ".DONE");
|
|
2979
3007
|
writeFileSync(donePath, `Completed: ${new Date().toISOString()}\nTask: ${task.taskId}\n`);
|
|
2980
3008
|
updateStatusField(statusPath, "Status", "✅ Complete");
|
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
TaskplaneConfig,
|
|
44
44
|
TaskRunnerSection,
|
|
45
45
|
OrchestratorSection,
|
|
46
|
+
WorkspaceSectionConfig,
|
|
46
47
|
UserPreferences,
|
|
47
48
|
} from "./config-schema.ts";
|
|
48
49
|
|
|
@@ -270,6 +271,71 @@ function mapOrchestratorYaml(raw: any): Partial<OrchestratorSection> {
|
|
|
270
271
|
return result;
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Normalize a workspace section loaded from JSON/YAML into camelCase shape.
|
|
276
|
+
*
|
|
277
|
+
* Compatibility: if `routing.taskPacketRepo` is missing, defaults to
|
|
278
|
+
* `routing.defaultRepo` and emits a warning message.
|
|
279
|
+
*/
|
|
280
|
+
function normalizeWorkspaceSection(
|
|
281
|
+
rawWorkspace: any,
|
|
282
|
+
sourcePath: string,
|
|
283
|
+
): WorkspaceSectionConfig | undefined {
|
|
284
|
+
if (!rawWorkspace || typeof rawWorkspace !== "object" || Array.isArray(rawWorkspace)) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const rawRepos = rawWorkspace.repos;
|
|
289
|
+
if (!rawRepos || typeof rawRepos !== "object" || Array.isArray(rawRepos)) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const rawRouting = rawWorkspace.routing;
|
|
294
|
+
if (!rawRouting || typeof rawRouting !== "object" || Array.isArray(rawRouting)) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const repos: WorkspaceSectionConfig["repos"] = {};
|
|
299
|
+
for (const [repoId, repoVal] of Object.entries(rawRepos as Record<string, any>)) {
|
|
300
|
+
if (!repoVal || typeof repoVal !== "object" || Array.isArray(repoVal)) continue;
|
|
301
|
+
const repoObj = repoVal as Record<string, any>;
|
|
302
|
+
if (typeof repoObj.path !== "string" || repoObj.path.trim() === "") continue;
|
|
303
|
+
repos[repoId] = {
|
|
304
|
+
path: repoObj.path,
|
|
305
|
+
...(typeof repoObj.defaultBranch === "string" && repoObj.defaultBranch.trim()
|
|
306
|
+
? { defaultBranch: repoObj.defaultBranch }
|
|
307
|
+
: {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const defaultRepo = typeof rawRouting.defaultRepo === "string" ? rawRouting.defaultRepo.trim() : "";
|
|
312
|
+
const tasksRoot = typeof rawRouting.tasksRoot === "string" ? rawRouting.tasksRoot.trim() : "";
|
|
313
|
+
let taskPacketRepo = typeof rawRouting.taskPacketRepo === "string" ? rawRouting.taskPacketRepo.trim() : "";
|
|
314
|
+
|
|
315
|
+
if (!taskPacketRepo && defaultRepo) {
|
|
316
|
+
taskPacketRepo = defaultRepo;
|
|
317
|
+
console.error(
|
|
318
|
+
`[taskplane] config compatibility: workspace.routing.taskPacketRepo is missing in ${sourcePath}; defaulting to workspace.routing.defaultRepo ('${defaultRepo}'). Add workspace.routing.taskPacketRepo explicitly.`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!tasksRoot || !defaultRepo || !taskPacketRepo) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const strict = rawRouting.strict === true;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
repos,
|
|
330
|
+
routing: {
|
|
331
|
+
tasksRoot,
|
|
332
|
+
defaultRepo,
|
|
333
|
+
taskPacketRepo,
|
|
334
|
+
...(strict ? { strict: true } : {}),
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
273
339
|
|
|
274
340
|
// ── Config File Path Resolution ──────────────────────────────────────
|
|
275
341
|
|
|
@@ -354,6 +420,12 @@ function loadJsonConfig(configRoot: string): TaskplaneConfig | null {
|
|
|
354
420
|
if (parsed.orchestrator) {
|
|
355
421
|
deepMerge(config.orchestrator, parsed.orchestrator);
|
|
356
422
|
}
|
|
423
|
+
if (parsed.workspace) {
|
|
424
|
+
const normalizedWorkspace = normalizeWorkspaceSection(parsed.workspace, jsonPath);
|
|
425
|
+
if (normalizedWorkspace) {
|
|
426
|
+
config.workspace = normalizedWorkspace;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
357
429
|
|
|
358
430
|
return config;
|
|
359
431
|
}
|
|
@@ -438,6 +510,29 @@ function loadOrchestratorYaml(configRoot: string): OrchestratorSection {
|
|
|
438
510
|
}
|
|
439
511
|
}
|
|
440
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Load optional workspace routing config from legacy `taskplane-workspace.yaml`.
|
|
515
|
+
*
|
|
516
|
+
* This file is fallback-only for workspace metadata when JSON `workspace`
|
|
517
|
+
* section is not present. Malformed files are ignored here — strict validation
|
|
518
|
+
* still happens in workspace runtime loading (`workspace.ts`).
|
|
519
|
+
*/
|
|
520
|
+
function loadWorkspaceYaml(configRoot: string): WorkspaceSectionConfig | undefined {
|
|
521
|
+
const yamlPath = resolveConfigFilePath(configRoot, "taskplane-workspace.yaml");
|
|
522
|
+
if (!existsSync(yamlPath)) return undefined;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const raw = readFileSync(yamlPath, "utf-8");
|
|
526
|
+
const loaded = yamlParse(raw) as any;
|
|
527
|
+
if (!loaded || typeof loaded !== "object") return undefined;
|
|
528
|
+
|
|
529
|
+
const converted = convertStructuralKeys(loaded);
|
|
530
|
+
return normalizeWorkspaceSection(converted, yamlPath);
|
|
531
|
+
} catch {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
441
536
|
|
|
442
537
|
// ── User Preferences (Layer 2) ───────────────────────────────────────
|
|
443
538
|
|
|
@@ -586,9 +681,17 @@ export function applyUserPreferences(config: TaskplaneConfig, prefs: UserPrefere
|
|
|
586
681
|
* in either location. This allows pointer-resolved roots (e.g.,
|
|
587
682
|
* `<configRepo>/.taskplane/`) where files are scaffolded directly
|
|
588
683
|
* without a `.pi/` subdirectory.
|
|
684
|
+
*
|
|
685
|
+
* Includes optional workspace YAML (`taskplane-workspace.yaml`) so
|
|
686
|
+
* workspace-only roots participate in config-root resolution.
|
|
589
687
|
*/
|
|
590
688
|
export function hasConfigFiles(root: string): boolean {
|
|
591
|
-
const files = [
|
|
689
|
+
const files = [
|
|
690
|
+
PROJECT_CONFIG_FILENAME,
|
|
691
|
+
"task-runner.yaml",
|
|
692
|
+
"task-orchestrator.yaml",
|
|
693
|
+
"taskplane-workspace.yaml",
|
|
694
|
+
];
|
|
592
695
|
for (const f of files) {
|
|
593
696
|
if (existsSync(join(root, ".pi", f)) || existsSync(join(root, f))) return true;
|
|
594
697
|
}
|
|
@@ -637,6 +740,7 @@ export function resolveConfigRoot(cwd: string, pointerConfigRoot?: string): stri
|
|
|
637
740
|
* Layer 1 — Project config:
|
|
638
741
|
* 1. `.pi/taskplane-config.json` — JSON-first (new format)
|
|
639
742
|
* 2. `.pi/task-runner.yaml` + `.pi/task-orchestrator.yaml` — YAML fallback
|
|
743
|
+
* (+ optional `.pi/taskplane-workspace.yaml` workspace section mapping)
|
|
640
744
|
* 3. Defaults — if no config files exist
|
|
641
745
|
*
|
|
642
746
|
* Layer 2 — User preferences (applied on top of Layer 1):
|
|
@@ -671,10 +775,12 @@ export function loadProjectConfig(cwd: string, pointerConfigRoot?: string): Task
|
|
|
671
775
|
// Fall back to YAML
|
|
672
776
|
const taskRunner = loadTaskRunnerYaml(configRoot);
|
|
673
777
|
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
778
|
+
const workspace = loadWorkspaceYaml(configRoot);
|
|
674
779
|
config = {
|
|
675
780
|
configVersion: CONFIG_VERSION,
|
|
676
781
|
taskRunner,
|
|
677
782
|
orchestrator,
|
|
783
|
+
...(workspace ? { workspace } : {}),
|
|
678
784
|
};
|
|
679
785
|
}
|
|
680
786
|
|
|
@@ -710,10 +816,12 @@ export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): Taskp
|
|
|
710
816
|
// Fall back to YAML
|
|
711
817
|
const taskRunner = loadTaskRunnerYaml(configRoot);
|
|
712
818
|
const orchestrator = loadOrchestratorYaml(configRoot);
|
|
819
|
+
const workspace = loadWorkspaceYaml(configRoot);
|
|
713
820
|
return {
|
|
714
821
|
configVersion: CONFIG_VERSION,
|
|
715
822
|
taskRunner,
|
|
716
823
|
orchestrator,
|
|
824
|
+
...(workspace ? { workspace } : {}),
|
|
717
825
|
};
|
|
718
826
|
}
|
|
719
827
|
|
|
@@ -415,6 +415,37 @@ export interface OrchestratorSection {
|
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
|
|
418
|
+
// ── Workspace Section Interfaces ─────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
/** Workspace repo definition (JSON config shape). */
|
|
421
|
+
export interface WorkspaceRepoSectionConfig {
|
|
422
|
+
/** Repo root path (relative to workspace root or absolute). */
|
|
423
|
+
path: string;
|
|
424
|
+
/** Optional default branch override. */
|
|
425
|
+
defaultBranch?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Workspace routing definition (JSON config shape). */
|
|
429
|
+
export interface WorkspaceRoutingSectionConfig {
|
|
430
|
+
/** Shared task packet root directory. */
|
|
431
|
+
tasksRoot: string;
|
|
432
|
+
/** Default repo for unqualified operations. */
|
|
433
|
+
defaultRepo: string;
|
|
434
|
+
/** Packet-home repo owning PROMPT/STATUS/.DONE. */
|
|
435
|
+
taskPacketRepo: string;
|
|
436
|
+
/** Strict repo routing mode. */
|
|
437
|
+
strict?: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Optional workspace section in taskplane-config.json. */
|
|
441
|
+
export interface WorkspaceSectionConfig {
|
|
442
|
+
/** Repo map keyed by repo ID. */
|
|
443
|
+
repos: Record<string, WorkspaceRepoSectionConfig>;
|
|
444
|
+
/** Routing contract for workspace mode. */
|
|
445
|
+
routing: WorkspaceRoutingSectionConfig;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
418
449
|
// ── Unified Config ───────────────────────────────────────────────────
|
|
419
450
|
|
|
420
451
|
/**
|
|
@@ -442,6 +473,8 @@ export interface TaskplaneConfig {
|
|
|
442
473
|
taskRunner: TaskRunnerSection;
|
|
443
474
|
/** Orchestrator settings */
|
|
444
475
|
orchestrator: OrchestratorSection;
|
|
476
|
+
/** Optional workspace config (JSON-first; legacy YAML fallback supported). */
|
|
477
|
+
workspace?: WorkspaceSectionConfig;
|
|
445
478
|
}
|
|
446
479
|
|
|
447
480
|
|
|
@@ -475,7 +475,7 @@ export function buildLaneEnvVars(
|
|
|
475
475
|
|
|
476
476
|
const vars: Record<string, string> = {
|
|
477
477
|
TASK_AUTOSTART: relativePath,
|
|
478
|
-
TASK_RUNNER_SPAWN_MODE: "
|
|
478
|
+
TASK_RUNNER_SPAWN_MODE: "tmux",
|
|
479
479
|
TASK_RUNNER_TMUX_PREFIX: lane.tmuxSessionName,
|
|
480
480
|
ORCH_SIDECAR_DIR: join(workspaceRoot || repoRoot, ".pi"),
|
|
481
481
|
NODE_PATH: nodePath,
|
|
@@ -915,6 +915,10 @@ export function spawnLaneSession(
|
|
|
915
915
|
|
|
916
916
|
// Build env vars
|
|
917
917
|
const envVars = buildLaneEnvVars(lane, task.task.promptPath, repoRoot, workspaceRoot);
|
|
918
|
+
// Pass batch ID so task-runner can include it in lane state for dashboard filtering
|
|
919
|
+
if (config.orchestrator?.batchId) {
|
|
920
|
+
envVars.ORCH_BATCH_ID = config.orchestrator.batchId;
|
|
921
|
+
}
|
|
918
922
|
if (extraEnvVars) {
|
|
919
923
|
Object.assign(envVars, extraEnvVars);
|
|
920
924
|
}
|
|
@@ -1457,6 +1457,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1457
1457
|
* and return early with a user-facing error when null.
|
|
1458
1458
|
*/
|
|
1459
1459
|
let execCtx: ExecutionContext | null = null;
|
|
1460
|
+
/** Last startup error message to surface consistently through command guards. */
|
|
1461
|
+
let execCtxInitError: string | null = null;
|
|
1460
1462
|
|
|
1461
1463
|
// ── Widget Rendering ─────────────────────────────────────────────
|
|
1462
1464
|
|
|
@@ -1477,17 +1479,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1477
1479
|
|
|
1478
1480
|
// ── Command Guard ────────────────────────────────────────────────
|
|
1479
1481
|
|
|
1482
|
+
function getExecCtxInitErrorMessage(): string {
|
|
1483
|
+
return execCtxInitError ??
|
|
1484
|
+
"❌ Orchestrator not initialized. Startup failed before execution context was created.\nRestart the session after fixing configuration/setup issues.";
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1480
1487
|
/**
|
|
1481
1488
|
* Guard: returns true if execution context is initialized, false otherwise.
|
|
1482
1489
|
* Emits a user-facing error notification when the context is missing.
|
|
1483
1490
|
*/
|
|
1484
1491
|
function requireExecCtx(ctx: ExtensionContext): boolean {
|
|
1485
1492
|
if (execCtx) return true;
|
|
1486
|
-
ctx.ui.notify(
|
|
1487
|
-
"❌ Orchestrator not initialized. Workspace configuration failed at startup.\n" +
|
|
1488
|
-
"Fix the workspace config or remove it to use repo mode, then restart.",
|
|
1489
|
-
"error",
|
|
1490
|
-
);
|
|
1493
|
+
ctx.ui.notify(getExecCtxInitErrorMessage(), "error");
|
|
1491
1494
|
return false;
|
|
1492
1495
|
}
|
|
1493
1496
|
|
|
@@ -1720,7 +1723,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1720
1723
|
|
|
1721
1724
|
if (!execCtx) {
|
|
1722
1725
|
return {
|
|
1723
|
-
message:
|
|
1726
|
+
message: getExecCtxInitErrorMessage(),
|
|
1724
1727
|
error: true,
|
|
1725
1728
|
};
|
|
1726
1729
|
}
|
|
@@ -2050,7 +2053,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2050
2053
|
function doOrchResume(force: boolean, ctx: ExtensionContext): { message: string; error?: boolean } {
|
|
2051
2054
|
if (!execCtx) {
|
|
2052
2055
|
return {
|
|
2053
|
-
message:
|
|
2056
|
+
message: getExecCtxInitErrorMessage(),
|
|
2054
2057
|
error: true,
|
|
2055
2058
|
};
|
|
2056
2059
|
}
|
|
@@ -2786,7 +2789,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2786
2789
|
): Promise<{ message: string; error?: boolean; level?: "info" | "warning" | "error" }> {
|
|
2787
2790
|
if (!execCtx) {
|
|
2788
2791
|
return {
|
|
2789
|
-
message:
|
|
2792
|
+
message: getExecCtxInitErrorMessage(),
|
|
2790
2793
|
error: true,
|
|
2791
2794
|
};
|
|
2792
2795
|
}
|
|
@@ -3653,24 +3656,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
3653
3656
|
orchWidgetCtx = ctx;
|
|
3654
3657
|
|
|
3655
3658
|
// ── Build execution context (config + workspace mode detection) ──
|
|
3656
|
-
// Reset
|
|
3659
|
+
// Reset startup state before loading to prevent stale errors on re-init.
|
|
3657
3660
|
execCtx = null;
|
|
3661
|
+
execCtxInitError = null;
|
|
3658
3662
|
try {
|
|
3659
3663
|
execCtx = buildExecutionContext(ctx.cwd, loadOrchestratorConfig, loadTaskRunnerConfig);
|
|
3660
3664
|
} catch (err: unknown) {
|
|
3661
3665
|
if (err instanceof WorkspaceConfigError) {
|
|
3662
|
-
//
|
|
3663
|
-
//
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3666
|
+
// Startup is fatal when workspace config is invalid OR repo-mode setup
|
|
3667
|
+
// requirements are not met (non-git cwd without workspace config).
|
|
3668
|
+
const setupError = err.code === "WORKSPACE_SETUP_REQUIRED";
|
|
3669
|
+
execCtxInitError = setupError
|
|
3670
|
+
? (
|
|
3671
|
+
`❌ Orchestrator startup blocked [${err.code}]\n\n` +
|
|
3672
|
+
`${err.message}\n\n` +
|
|
3673
|
+
`Orchestrator commands are disabled until this setup issue is resolved.`
|
|
3674
|
+
)
|
|
3675
|
+
: (
|
|
3676
|
+
`❌ Workspace configuration error [${err.code}]\n\n` +
|
|
3677
|
+
`${err.message}\n\n` +
|
|
3678
|
+
`Fix the workspace config at .pi/taskplane-workspace.yaml (or taskplane-config.json workspace section), then restart.\n` +
|
|
3679
|
+
`Orchestrator commands are disabled until this is resolved.`
|
|
3680
|
+
);
|
|
3681
|
+
|
|
3682
|
+
ctx.ui.notify(execCtxInitError, "error");
|
|
3671
3683
|
ctx.ui.setStatus(
|
|
3672
3684
|
"task-orchestrator",
|
|
3673
|
-
|
|
3685
|
+
setupError
|
|
3686
|
+
? "🔀 Orchestrator · ❌ startup failed (setup required)"
|
|
3687
|
+
: "🔀 Orchestrator · ❌ startup failed (workspace config error)",
|
|
3674
3688
|
);
|
|
3675
3689
|
return;
|
|
3676
3690
|
}
|
|
@@ -2832,10 +2832,11 @@ export const BATCH_HISTORY_MAX_ENTRIES = 100;
|
|
|
2832
2832
|
* coordinates multiple repos and a shared task root.
|
|
2833
2833
|
*
|
|
2834
2834
|
* Mode determination rules:
|
|
2835
|
-
* 1.
|
|
2836
|
-
* 2. Workspace config file present + invalid → fatal error with actionable
|
|
2835
|
+
* 1. Workspace config file present + invalid → fatal error with actionable
|
|
2837
2836
|
* `WorkspaceConfigError` (never silently falls back to repo mode).
|
|
2838
|
-
*
|
|
2837
|
+
* 2. Workspace config file present + valid → workspace mode.
|
|
2838
|
+
* 3. No workspace config + cwd is a git repo → repo mode.
|
|
2839
|
+
* 4. No workspace config + cwd is not a git repo → `WORKSPACE_SETUP_REQUIRED`.
|
|
2839
2840
|
*/
|
|
2840
2841
|
export type WorkspaceMode = "repo" | "workspace";
|
|
2841
2842
|
|
|
@@ -2872,6 +2873,15 @@ export interface WorkspaceRoutingConfig {
|
|
|
2872
2873
|
* Must reference a valid key in `WorkspaceConfig.repos`.
|
|
2873
2874
|
*/
|
|
2874
2875
|
defaultRepo: string;
|
|
2876
|
+
/**
|
|
2877
|
+
* Repo ID that owns task packet files (PROMPT.md/STATUS.md/.DONE/.reviews).
|
|
2878
|
+
*
|
|
2879
|
+
* Required at runtime. Legacy workspace YAML without this field is
|
|
2880
|
+
* compatibility-mapped to `defaultRepo` during load with a warning.
|
|
2881
|
+
*
|
|
2882
|
+
* Invariant: `tasksRoot` must resolve inside `repos[taskPacketRepo].path`.
|
|
2883
|
+
*/
|
|
2884
|
+
taskPacketRepo: string;
|
|
2875
2885
|
/**
|
|
2876
2886
|
* When true, every task MUST declare an explicit execution target
|
|
2877
2887
|
* (via `## Execution Target` section or inline `**Repo:**` in PROMPT.md).
|
|
@@ -2964,6 +2974,10 @@ export interface ExecutionContext {
|
|
|
2964
2974
|
* - WORKSPACE_TASKS_ROOT_NOT_FOUND: `routing.tasks_root` path does not exist on disk
|
|
2965
2975
|
* - WORKSPACE_MISSING_DEFAULT_REPO: `routing.default_repo` is missing or empty
|
|
2966
2976
|
* - WORKSPACE_DEFAULT_REPO_NOT_FOUND: `routing.default_repo` references a repo ID not in the repos map
|
|
2977
|
+
* - WORKSPACE_TASK_PACKET_REPO_NOT_FOUND: `routing.task_packet_repo` references a repo ID not in the repos map
|
|
2978
|
+
* - WORKSPACE_TASKS_ROOT_OUTSIDE_PACKET_REPO: `routing.tasks_root` resolves outside `repos[routing.task_packet_repo].path`
|
|
2979
|
+
* - WORKSPACE_TASK_AREA_OUTSIDE_TASKS_ROOT: A configured task-area path resolves outside `routing.tasks_root`
|
|
2980
|
+
* - WORKSPACE_SETUP_REQUIRED: No workspace config and cwd is not a git repository
|
|
2967
2981
|
* - WORKSPACE_DUPLICATE_REPO_PATH: Two or more repos share the same filesystem path
|
|
2968
2982
|
* - WORKSPACE_SCHEMA_INVALID: Config file has valid YAML but missing/invalid top-level structure
|
|
2969
2983
|
*/
|
|
@@ -2978,10 +2992,12 @@ export type WorkspaceConfigErrorCode =
|
|
|
2978
2992
|
| "WORKSPACE_TASKS_ROOT_NOT_FOUND"
|
|
2979
2993
|
| "WORKSPACE_MISSING_DEFAULT_REPO"
|
|
2980
2994
|
| "WORKSPACE_DEFAULT_REPO_NOT_FOUND"
|
|
2995
|
+
| "WORKSPACE_TASK_PACKET_REPO_NOT_FOUND"
|
|
2996
|
+
| "WORKSPACE_TASKS_ROOT_OUTSIDE_PACKET_REPO"
|
|
2997
|
+
| "WORKSPACE_TASK_AREA_OUTSIDE_TASKS_ROOT"
|
|
2998
|
+
| "WORKSPACE_SETUP_REQUIRED"
|
|
2981
2999
|
| "WORKSPACE_DUPLICATE_REPO_PATH"
|
|
2982
|
-
| "WORKSPACE_SCHEMA_INVALID"
|
|
2983
|
-
|
|
2984
|
-
/**
|
|
3000
|
+
| "WORKSPACE_SCHEMA_INVALID";/**
|
|
2985
3001
|
* Typed error class for workspace configuration failures.
|
|
2986
3002
|
*
|
|
2987
3003
|
* Thrown during workspace config loading/validation when the config file
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Workspace configuration loading and validation.
|
|
3
3
|
*
|
|
4
4
|
* Detects workspace mode by checking for `.pi/taskplane-workspace.yaml`.
|
|
5
|
-
* When the file is absent, the orchestrator runs in repo mode (default).
|
|
6
5
|
* When the file is present, it must be valid — invalid files are fatal.
|
|
6
|
+
* When absent, `loadWorkspaceConfig()` returns null and `buildExecutionContext()`
|
|
7
|
+
* decides repo-mode eligibility (cwd must be a git repository).
|
|
7
8
|
*
|
|
8
9
|
* Validation order (deterministic, fail-fast):
|
|
9
10
|
* 1. File existence check → absent = repo mode (return null)
|
|
@@ -20,6 +21,8 @@
|
|
|
20
21
|
* 9. routing.tasks_root exists → WORKSPACE_TASKS_ROOT_NOT_FOUND
|
|
21
22
|
* 10. routing.default_repo present → WORKSPACE_MISSING_DEFAULT_REPO
|
|
22
23
|
* 11. routing.default_repo valid → WORKSPACE_DEFAULT_REPO_NOT_FOUND
|
|
24
|
+
* 12. routing.task_packet_repo valid (or compat fallback) → WORKSPACE_TASK_PACKET_REPO_NOT_FOUND
|
|
25
|
+
* 13. routing.tasks_root inside packet-home repo → WORKSPACE_TASKS_ROOT_OUTSIDE_PACKET_REPO
|
|
23
26
|
*
|
|
24
27
|
* Path normalization rules:
|
|
25
28
|
* - Relative paths are resolved against workspaceRoot.
|
|
@@ -95,6 +98,16 @@ function resolveAbsolutePath(p: string, base: string): string {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/**
|
|
102
|
+
* True when `childPath` is the same path as `parentPath` or contained within it.
|
|
103
|
+
* Uses canonicalized paths for cross-platform, case-insensitive comparison.
|
|
104
|
+
*/
|
|
105
|
+
function isPathWithinContainer(childPath: string, parentPath: string): boolean {
|
|
106
|
+
const child = canonicalizePath(childPath, "");
|
|
107
|
+
const parent = canonicalizePath(parentPath, "");
|
|
108
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
|
|
99
112
|
// ── Pointer Resolution ───────────────────────────────────────────────
|
|
100
113
|
|
|
@@ -498,7 +511,50 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
|
|
|
498
511
|
);
|
|
499
512
|
}
|
|
500
513
|
|
|
501
|
-
//
|
|
514
|
+
// 12. routing.task_packet_repo (required by v1 contract)
|
|
515
|
+
// Compatibility policy: if omitted, default to routing.default_repo and
|
|
516
|
+
// emit a warning so legacy configs remain deterministic.
|
|
517
|
+
const hasTaskPacketRepo = Object.prototype.hasOwnProperty.call(rawRouting, "task_packet_repo");
|
|
518
|
+
const rawTaskPacketRepo = rawRouting.task_packet_repo;
|
|
519
|
+
let taskPacketRepoId = defaultRepoId;
|
|
520
|
+
|
|
521
|
+
if (hasTaskPacketRepo) {
|
|
522
|
+
if (typeof rawTaskPacketRepo !== "string" || rawTaskPacketRepo.trim() === "") {
|
|
523
|
+
throw new WorkspaceConfigError(
|
|
524
|
+
"WORKSPACE_SCHEMA_INVALID",
|
|
525
|
+
"Workspace config 'routing.task_packet_repo' must be a non-empty string when provided.",
|
|
526
|
+
undefined,
|
|
527
|
+
configFile,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
taskPacketRepoId = rawTaskPacketRepo.trim();
|
|
531
|
+
} else {
|
|
532
|
+
console.error(
|
|
533
|
+
`[taskplane] workspace compatibility: 'routing.task_packet_repo' is missing in ${configFile}; defaulting to routing.default_repo ('${defaultRepoId}'). Add 'routing.task_packet_repo' explicitly.`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!repos.has(taskPacketRepoId)) {
|
|
538
|
+
throw new WorkspaceConfigError(
|
|
539
|
+
"WORKSPACE_TASK_PACKET_REPO_NOT_FOUND",
|
|
540
|
+
`routing.task_packet_repo '${taskPacketRepoId}' does not match any repo ID. Available repos: ${Array.from(repos.keys()).join(", ")}`,
|
|
541
|
+
undefined,
|
|
542
|
+
configFile,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 13. tasks_root must be inside repos[task_packet_repo].path
|
|
547
|
+
const packetRepoPath = repos.get(taskPacketRepoId)!.path;
|
|
548
|
+
if (!isPathWithinContainer(tasksRootAbsolute, packetRepoPath)) {
|
|
549
|
+
throw new WorkspaceConfigError(
|
|
550
|
+
"WORKSPACE_TASKS_ROOT_OUTSIDE_PACKET_REPO",
|
|
551
|
+
`routing.tasks_root '${tasksRootAbsolute}' must be inside packet-home repo '${taskPacketRepoId}' (${packetRepoPath}). Update routing.tasks_root or routing.task_packet_repo.`,
|
|
552
|
+
undefined,
|
|
553
|
+
tasksRootAbsolute,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── 14. routing.strict (optional boolean, default false) ─────
|
|
502
558
|
const rawStrict = rawRouting.strict;
|
|
503
559
|
if (rawStrict !== undefined) {
|
|
504
560
|
// null (from bare `strict:` or `strict: null` in YAML) is rejected
|
|
@@ -518,6 +574,7 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
|
|
|
518
574
|
const routing: WorkspaceRoutingConfig = {
|
|
519
575
|
tasksRoot: tasksRootAbsolute,
|
|
520
576
|
defaultRepo: defaultRepoId,
|
|
577
|
+
taskPacketRepo: taskPacketRepoId,
|
|
521
578
|
...(strict ? { strict: true } : {}),
|
|
522
579
|
};
|
|
523
580
|
|
|
@@ -531,6 +588,39 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
|
|
|
531
588
|
}
|
|
532
589
|
|
|
533
590
|
|
|
591
|
+
// ── Cross-Config Validation ─────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Enforce that every configured task area resolves inside workspace routing.tasksRoot.
|
|
595
|
+
*
|
|
596
|
+
* This is a cross-config invariant and therefore runs after both workspace and
|
|
597
|
+
* task-runner configs are loaded.
|
|
598
|
+
*/
|
|
599
|
+
export function validateTaskAreasWithinTasksRoot(
|
|
600
|
+
workspaceRoot: string,
|
|
601
|
+
workspaceConfig: WorkspaceConfig,
|
|
602
|
+
taskRunnerConfig: import("./types.ts").TaskRunnerConfig,
|
|
603
|
+
): void {
|
|
604
|
+
const tasksRoot = workspaceConfig.routing.tasksRoot;
|
|
605
|
+
const areaEntries = Object.entries(taskRunnerConfig.task_areas ?? {}).sort((a, b) =>
|
|
606
|
+
a[0].localeCompare(b[0])
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
for (const [areaName, area] of areaEntries) {
|
|
610
|
+
const areaPathRaw = (area?.path ?? "").trim();
|
|
611
|
+
const areaAbsolute = resolveAbsolutePath(areaPathRaw, workspaceRoot);
|
|
612
|
+
if (!isPathWithinContainer(areaAbsolute, tasksRoot)) {
|
|
613
|
+
throw new WorkspaceConfigError(
|
|
614
|
+
"WORKSPACE_TASK_AREA_OUTSIDE_TASKS_ROOT",
|
|
615
|
+
`Task area '${areaName}' path '${areaAbsolute}' must be inside routing.tasks_root '${tasksRoot}'. Move the area under tasks_root or update task_areas.${areaName}.path.`,
|
|
616
|
+
undefined,
|
|
617
|
+
areaAbsolute,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
534
624
|
// ── Execution Context Builder ────────────────────────────────────────
|
|
535
625
|
|
|
536
626
|
/**
|
|
@@ -543,8 +633,14 @@ export function loadWorkspaceConfig(workspaceRoot: string): WorkspaceConfig | nu
|
|
|
543
633
|
* @param loadOrchConfig - Orchestrator config loader (for testability)
|
|
544
634
|
* @param loadTaskConfig - Task runner config loader (for testability)
|
|
545
635
|
* @returns ExecutionContext ready for orchestrator consumption
|
|
546
|
-
* @throws WorkspaceConfigError if workspace config is present but invalid
|
|
636
|
+
* @throws WorkspaceConfigError if workspace config is present but invalid,
|
|
637
|
+
* or when no workspace config exists and `cwd` is not a git repository.
|
|
547
638
|
*/
|
|
639
|
+
function isInsideGitRepo(cwd: string): boolean {
|
|
640
|
+
const probe = runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
641
|
+
return probe.ok && probe.stdout.trim() === "true";
|
|
642
|
+
}
|
|
643
|
+
|
|
548
644
|
export function buildExecutionContext(
|
|
549
645
|
cwd: string,
|
|
550
646
|
loadOrchConfig: (root: string, pointerConfigRoot?: string) => import("./types.ts").OrchestratorConfig,
|
|
@@ -553,6 +649,19 @@ export function buildExecutionContext(
|
|
|
553
649
|
const workspaceConfig = loadWorkspaceConfig(cwd);
|
|
554
650
|
|
|
555
651
|
if (workspaceConfig === null) {
|
|
652
|
+
// Deterministic mode guard: without workspace config, repo mode is only
|
|
653
|
+
// valid when cwd is a git repository.
|
|
654
|
+
if (!isInsideGitRepo(cwd)) {
|
|
655
|
+
const wsConfigFile = workspaceConfigPath(cwd);
|
|
656
|
+
throw new WorkspaceConfigError(
|
|
657
|
+
"WORKSPACE_SETUP_REQUIRED",
|
|
658
|
+
`No workspace config found at ${wsConfigFile}, and current directory is not a git repository: ${cwd}. ` +
|
|
659
|
+
`Run Taskplane from a git repository, or create ${wsConfigFile} (taskplane init) to use workspace mode.`,
|
|
660
|
+
undefined,
|
|
661
|
+
cwd,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
556
665
|
// Repo mode: pointer is ignored entirely. Config loads from cwd.
|
|
557
666
|
const orchestratorConfig = loadOrchConfig(cwd);
|
|
558
667
|
const taskRunnerConfig = loadTaskConfig(cwd);
|
|
@@ -580,6 +689,9 @@ export function buildExecutionContext(
|
|
|
580
689
|
const orchestratorConfig = loadOrchConfig(cwd, pointerConfigRoot);
|
|
581
690
|
const taskRunnerConfig = loadTaskConfig(cwd, pointerConfigRoot);
|
|
582
691
|
|
|
692
|
+
// Cross-config invariant: every task-area path must live under routing.tasks_root.
|
|
693
|
+
validateTaskAreasWithinTasksRoot(cwd, workspaceConfig, taskRunnerConfig);
|
|
694
|
+
|
|
583
695
|
const defaultRepo = workspaceConfig.repos.get(workspaceConfig.routing.defaultRepo)!;
|
|
584
696
|
return {
|
|
585
697
|
workspaceRoot: cwd,
|
package/package.json
CHANGED
|
@@ -354,6 +354,30 @@ files and make sure their file scopes reflect that.
|
|
|
354
354
|
|
|
355
355
|
---
|
|
356
356
|
|
|
357
|
+
## Preventing Empty Completions
|
|
358
|
+
|
|
359
|
+
Workers can shortcut tasks by observing that existing code "already satisfies"
|
|
360
|
+
requirements and checking off items without implementing anything. This is the
|
|
361
|
+
most dangerous failure mode — it produces false completions that waste the entire
|
|
362
|
+
pipeline.
|
|
363
|
+
|
|
364
|
+
**Defense: Make deliverables concrete and verifiable.**
|
|
365
|
+
|
|
366
|
+
| ❌ Vague (shortcuttable) | ✅ Concrete (verifiable) |
|
|
367
|
+
|--------------------------|------------------------|
|
|
368
|
+
| "Add taskPacketRepo support" | "Add `taskPacketRepo` field to `WorkspaceRoutingConfig` in types.ts" |
|
|
369
|
+
| "Enforce mode selection" | "Add `validateWorkspaceMode()` function in workspace.ts that throws on invalid state" |
|
|
370
|
+
| "Update config loading" | "Modify `loadWorkspaceConfig()` to parse and validate `taskPacketRepo` from JSON config" |
|
|
371
|
+
| "Add tests" | "Create `tests/packet-home-contract.test.ts` with tests for: valid config, missing field error, invariant violation" |
|
|
372
|
+
|
|
373
|
+
**Rules for task creators:**
|
|
374
|
+
- Every implementation step MUST name specific files to create or modify
|
|
375
|
+
- "Add X" means "write new code that doesn't exist yet" — if it might already exist, say "verify X exists and add tests, or implement if missing"
|
|
376
|
+
- Include at least one NEW test file per task — workers can't shortcut test creation
|
|
377
|
+
- Each step's artifacts list must include at least one source file (not just STATUS.md)
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
357
381
|
## Key Principles
|
|
358
382
|
|
|
359
383
|
- **Documentation in every task.** Without "Must Update" and "Check If Affected"
|
|
@@ -173,6 +173,15 @@ When a reviewer returns REVISE with specific feedback items:
|
|
|
173
173
|
- Do NOT expand task scope beyond what the steps require
|
|
174
174
|
- If you discover something out of scope, note it in STATUS.md Discoveries table
|
|
175
175
|
|
|
176
|
+
## Completion Integrity
|
|
177
|
+
|
|
178
|
+
**Every checked checkbox MUST correspond to a real code change, test, or document edit.** You must NOT check off items by simply observing that existing code appears to satisfy them. Specifically:
|
|
179
|
+
|
|
180
|
+
- **If you believe work is already done:** You must still verify by running tests against the specific requirements AND document what you verified. Check off the item only after confirming with evidence (test output, code inspection notes in STATUS.md).
|
|
181
|
+
- **"No source files changed" is a red flag.** If you complete a task without modifying any source files (only STATUS.md), something is wrong. Every implementation task requires code changes. If you genuinely believe no changes are needed, log a detailed explanation in STATUS.md Discoveries and escalate — do NOT mark the task as complete.
|
|
182
|
+
- **A step that requires "Add X to Y" means you write the code.** Reading existing code and deciding it already satisfies the requirement is not implementation. If the existing code truly covers it, write a test that proves it, and document the finding.
|
|
183
|
+
- **Checking boxes without doing work is the most serious failure mode.** It wastes the entire batch pipeline (review, merge, integration) and produces a false completion that blocks dependent tasks.
|
|
184
|
+
|
|
176
185
|
## Review Protocol
|
|
177
186
|
|
|
178
187
|
If you have access to a `review_step` tool, use it at step boundaries to spawn
|