taskplane 0.3.1 → 0.4.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.
@@ -23,6 +23,12 @@ const MAX_PORT_ATTEMPTS = 20;
23
23
  const POLL_INTERVAL = 2000; // ms between state checks
24
24
 
25
25
  // REPO_ROOT is resolved after parseArgs() — see initialization below.
26
+ // In workspace mode, REPO_ROOT is the workspace root (passed via --root).
27
+ // All dashboard state paths (batch-state, lane-state, conversation logs,
28
+ // batch-history) live at <REPO_ROOT>/.pi/ — this is runtime/sidecar state
29
+ // which does NOT follow the taskplane-pointer.json resolution chain.
30
+ // The pointer directs config/agent lookups to a config repo, but the
31
+ // dashboard only reads state files, so no pointer resolution is needed here.
26
32
  let REPO_ROOT;
27
33
  let BATCH_STATE_PATH;
28
34
  let BATCH_HISTORY_PATH;
@@ -630,7 +636,13 @@ async function findPort(server, start, explicit) {
630
636
  async function main() {
631
637
  const opts = parseArgs();
632
638
 
633
- // Resolve project root: --root flag > cwd
639
+ // Resolve project root: --root flag > cwd.
640
+ // In workspace mode this is the workspace root. All state/sidecar files
641
+ // (batch-state, lane-state, conversation logs, batch-history) live at
642
+ // <REPO_ROOT>/.pi/ and are NOT affected by taskplane-pointer.json.
643
+ // The pointer only redirects config/agent resolution in task-runner and
644
+ // orchestrator — the dashboard reads only runtime state, so no pointer
645
+ // resolution is performed here.
634
646
  REPO_ROOT = path.resolve(opts.root || process.cwd());
635
647
  BATCH_STATE_PATH = path.join(REPO_ROOT, ".pi", "batch-state.json");
636
648
  BATCH_HISTORY_PATH = path.join(REPO_ROOT, ".pi", "batch-history.json");
@@ -26,7 +26,9 @@ import {
26
26
  } from "fs";
27
27
  import { tmpdir } from "os";
28
28
  import { join, dirname, basename, resolve } from "path";
29
- import { parse as yamlParse } from "yaml";
29
+ import { loadProjectConfig, toTaskConfig } from "./taskplane/config-loader.ts";
30
+ import { loadWorkspaceConfig, resolvePointer } from "./taskplane/workspace.ts";
31
+ import type { PointerResolution } from "./taskplane/types.ts";
30
32
 
31
33
 
32
34
  // ── Types ────────────────────────────────────────────────────────────
@@ -137,54 +139,75 @@ const DEFAULT_CONFIG: TaskConfig = {
137
139
  },
138
140
  };
139
141
 
140
- function loadConfig(cwd: string): TaskConfig {
141
- let configPath = join(cwd, ".pi", "task-runner.yaml");
142
- // In workspace mode, the worker runs in a repo worktree not the workspace root.
143
- // TASKPLANE_WORKSPACE_ROOT tells us where .pi/task-runner.yaml actually lives.
144
- if (!existsSync(configPath) && process.env.TASKPLANE_WORKSPACE_ROOT) {
145
- configPath = join(process.env.TASKPLANE_WORKSPACE_ROOT, ".pi", "task-runner.yaml");
146
- }
147
- if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
142
+ // ── Pointer Resolution (Workspace Mode) ──────────────────────────────
143
+
144
+ /** Track whether a pointer warning has been logged this session (log once). */
145
+ let _pointerWarningLogged = false;
146
+
147
+ /**
148
+ * Resolve the workspace pointer for config and agent path redirection.
149
+ *
150
+ * In workspace mode (TASKPLANE_WORKSPACE_ROOT set), reads the pointer
151
+ * file and resolves config/agent roots to the config repo. In repo mode,
152
+ * returns null (no pointer resolution needed).
153
+ *
154
+ * All pointer failures are non-fatal: missing, malformed, or invalid
155
+ * pointer files produce a warning and fall back to existing paths.
156
+ * Warning is logged to stderr once per session for operator visibility.
157
+ *
158
+ * @returns PointerResolution with resolved paths, or null in repo mode
159
+ */
160
+ function resolveTaskRunnerPointer(): PointerResolution | null {
161
+ const wsRoot = process.env.TASKPLANE_WORKSPACE_ROOT;
162
+ if (!wsRoot) return null; // repo mode — no pointer needed
163
+
148
164
  try {
149
- const raw = readFileSync(configPath, "utf-8");
150
- const loaded = yamlParse(raw) as any;
151
- // Parse standards_overrides: Record<areaName, { docs?, rules? }>
152
- const rawOverrides = loaded?.standards_overrides || {};
153
- const parsedOverrides: Record<string, { docs?: string[]; rules?: string[] }> = {};
154
- for (const [key, val] of Object.entries(rawOverrides)) {
155
- if (val && typeof val === "object") {
156
- const v = val as any;
157
- parsedOverrides[key] = {
158
- docs: Array.isArray(v.docs) ? v.docs : undefined,
159
- rules: Array.isArray(v.rules) ? v.rules : undefined,
160
- };
161
- }
162
- }
165
+ const wsConfig = loadWorkspaceConfig(wsRoot);
166
+ const result = resolvePointer(wsRoot, wsConfig);
163
167
 
164
- // Parse task_areas minimally (we only need path for standards resolution)
165
- const rawAreas = loaded?.task_areas || {};
166
- const parsedAreas: Record<string, { path: string }> = {};
167
- for (const [key, val] of Object.entries(rawAreas)) {
168
- if (val && typeof val === "object" && (val as any).path) {
169
- parsedAreas[key] = { path: (val as any).path };
170
- }
168
+ // Surface pointer warnings once per session for operator visibility
169
+ if (result?.warning && !_pointerWarningLogged) {
170
+ _pointerWarningLogged = true;
171
+ console.error(`[task-runner] pointer: ${result.warning}`);
171
172
  }
172
173
 
173
- return {
174
- project: { ...DEFAULT_CONFIG.project, ...loaded?.project },
175
- paths: { ...DEFAULT_CONFIG.paths, ...loaded?.paths },
176
- testing: { commands: { ...DEFAULT_CONFIG.testing.commands, ...loaded?.testing?.commands } },
177
- standards: {
178
- docs: loaded?.standards?.docs || DEFAULT_CONFIG.standards.docs,
179
- rules: loaded?.standards?.rules || DEFAULT_CONFIG.standards.rules,
180
- },
181
- standards_overrides: parsedOverrides,
182
- task_areas: parsedAreas,
183
- worker: { ...DEFAULT_CONFIG.worker, ...loaded?.worker },
184
- reviewer: { ...DEFAULT_CONFIG.reviewer, ...loaded?.reviewer },
185
- context: { ...DEFAULT_CONFIG.context, ...loaded?.context },
186
- };
174
+ return result;
187
175
  } catch {
176
+ // Workspace config load failure — fall back gracefully
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /** Reset pointer warning state (for testing only). */
182
+ export function _resetPointerWarning(): void {
183
+ _pointerWarningLogged = false;
184
+ }
185
+
186
+ /** Expose loadAgentDef for testing (not part of public API). */
187
+ export const _loadAgentDef = (cwd: string, name: string) => loadAgentDef(cwd, name);
188
+
189
+ /**
190
+ * Load task-runner config via the unified config loader.
191
+ *
192
+ * Reads `.pi/taskplane-config.json` first; falls back to YAML files;
193
+ * then defaults. Returns the legacy snake_case TaskConfig shape so all
194
+ * downstream consumers remain unchanged.
195
+ *
196
+ * Config root resolution order (workspace mode with pointer):
197
+ * 1. cwd has config files → use cwd (local override)
198
+ * 2. Pointer-resolved config root has config files → use it
199
+ * 3. TASKPLANE_WORKSPACE_ROOT has config files → use it (legacy fallback)
200
+ * 4. Fall back to cwd (loaders will return defaults)
201
+ *
202
+ * Repo mode: pointer is ignored, existing behavior unchanged.
203
+ */
204
+ export function loadConfig(cwd: string): TaskConfig {
205
+ try {
206
+ const pointer = resolveTaskRunnerPointer();
207
+ const unified = loadProjectConfig(cwd, pointer?.configRoot);
208
+ return toTaskConfig(unified);
209
+ } catch {
210
+ // If config loading fails (e.g., malformed JSON), fall back to defaults
188
211
  return { ...DEFAULT_CONFIG };
189
212
  }
190
213
  }
@@ -428,18 +451,30 @@ function resolveBaseAgentPath(name: string): string {
428
451
  *
429
452
  * Inheritance model (default: compose base + local):
430
453
  * 1. Load base agent from the shipped package (templates/agents/{name}.md)
431
- * 2. Load local agent from .pi/agents/{name}.md (if it exists)
454
+ * 2. Load local agent from .pi/agents/{name}.md (if it exists) — or from
455
+ * the pointer-resolved agent root in workspace mode
432
456
  * 3. If local file has `standalone: true` in frontmatter, use it as-is (no base)
433
457
  * 4. Otherwise, compose: base prompt + separator + local content
434
458
  * 5. Local frontmatter values (tools, model) override base values
435
459
  *
436
- * If no local file exists, the base file is used directly.
460
+ * Local override resolution order:
461
+ * 1. `<cwd>/.pi/agents/{name}.md` — worktree/repo local override (always first)
462
+ * 2. `<cwd>/agents/{name}.md` — worktree/repo local override (legacy location)
463
+ * 3. `<pointerAgentRoot>/{name}.md` — pointer-resolved config repo agents (workspace mode)
464
+ * First found wins. If none found, base file is used directly.
465
+ *
437
466
  * If no base file exists (e.g., custom agent), local file is used as-is.
438
467
  */
439
468
  function loadAgentDef(cwd: string, name: string): { systemPrompt: string; tools: string; model: string } | null {
440
469
  const basePath = resolveBaseAgentPath(name);
441
470
  const localPaths = [join(cwd, ".pi", "agents", `${name}.md`), join(cwd, "agents", `${name}.md`)];
442
471
 
472
+ // In workspace mode, add pointer-resolved agent root as fallback
473
+ const pointer = resolveTaskRunnerPointer();
474
+ if (pointer?.agentRoot) {
475
+ localPaths.push(join(pointer.agentRoot, `${name}.md`));
476
+ }
477
+
443
478
  // Load base from package
444
479
  const baseDef = parseAgentFile(basePath);
445
480