pi-orchestrator-config 4.24.1 → 4.24.2
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/CHANGELOG.md +14 -0
- package/extensions/orchestrator/subagent-tool.ts +85 -46
- package/extensions/pitasks/index.ts +8 -0
- package/extensions/shared/system-note.ts +44 -0
- package/extensions/shared/task-store-bridge.ts +43 -0
- package/package.json +1 -1
- package/rules/00-orchestrator-core.md +2 -1
- package/rules/40-critical-rules.md +1 -1
- package/rules/60-task-tracking.md +27 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.24.2] - 2026-11-04
|
|
4
|
+
|
|
5
|
+
- Async work is tracked even when an agent starts it: the task trigger now covers work set in
|
|
6
|
+
motion by an async result, and `taskId: "-1"` is restricted to a single read-only check in
|
|
7
|
+
every place that taught the old habit.
|
|
8
|
+
- Task-id validation actually runs. `pitasks` exports the store as a live binding that the
|
|
9
|
+
subagent tool used to snapshot before it was assigned, so an unknown id was accepted; the
|
|
10
|
+
store is now published on a symbol bridge and read at access time. Verified live: unknown
|
|
11
|
+
ids rejected, a real id and the read-only sentinel accepted.
|
|
12
|
+
- Machine-generated notices declare their provenance: follow-ups are prefixed
|
|
13
|
+
"[system message — not written by the user]", so a guard rail cannot be read as user input.
|
|
14
|
+
- Board updates are success-gated — a failed agent keeps its task open — and reconciling the
|
|
15
|
+
board before new dispatch is mandatory. Status reports derive from `TaskList`.
|
|
16
|
+
|
|
3
17
|
## [4.24.1] - 2026-11-03
|
|
4
18
|
|
|
5
19
|
- Dreaming no longer stores re-derivable changelog: `[done]` takes non-derivable outcomes only (conclusions, measurements, root causes) and reorganizing prunes PR/version/ticket
|
|
@@ -16,6 +16,8 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { createLogger } from "../shared/logger.js";
|
|
19
|
+
import { sendSystemNote } from "../shared/system-note.js";
|
|
20
|
+
import { publishedTaskStore } from "../shared/task-store-bridge.js";
|
|
19
21
|
import { createBridgedUi } from "../shared/ui-bridge.js";
|
|
20
22
|
import {
|
|
21
23
|
getMarkdownTheme,
|
|
@@ -30,12 +32,44 @@ import {
|
|
|
30
32
|
import { Type } from "typebox";
|
|
31
33
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
32
34
|
|
|
33
|
-
// Task store access via
|
|
34
|
-
let
|
|
35
|
+
// Task store access via the pitasks module.
|
|
36
|
+
// `taskStore` is `export let` — a live binding the pitasks extension assigns when it
|
|
37
|
+
// initializes. Copying the value at import time captured `undefined` and disabled
|
|
38
|
+
// task-id validation for the whole session (issue #1016), so the binding is read fresh.
|
|
39
|
+
let _pitasksModule: any = null;
|
|
40
|
+
let _pitasksLoad: Promise<any> | null = null;
|
|
41
|
+
async function pitasksModule(): Promise<any> {
|
|
42
|
+
if (_pitasksModule) return _pitasksModule;
|
|
43
|
+
if (!_pitasksLoad) {
|
|
44
|
+
_pitasksLoad = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
_pitasksModule = await import("../pitasks/index.js");
|
|
47
|
+
return _pitasksModule;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log.warn(`[subagent] pitasks module unavailable: ${String((err as Error)?.message ?? err).slice(0, 120)}`);
|
|
50
|
+
_pitasksLoad = null; // retry on the next call
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
}
|
|
55
|
+
return _pitasksLoad;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The live task store, or null while pitasks has not initialized it yet.
|
|
59
|
+
* Read from the global symbol registry: this tool's module instance cannot see the
|
|
60
|
+
* pitasks extension's `export let taskStore` binding (it snapshots `undefined`), which
|
|
61
|
+
* is why task-id validation silently failed open (issue #1016). */
|
|
35
62
|
async function getTaskStore(): Promise<any> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
const published = publishedTaskStore();
|
|
64
|
+
if (published) return published;
|
|
65
|
+
// Fall back to the module binding for in-process loads that share this instance.
|
|
66
|
+
const mod = await pitasksModule();
|
|
67
|
+
const store = mod?.taskStore ?? null;
|
|
68
|
+
if (!store) {
|
|
69
|
+
// Validation is about to be skipped: that is a degraded state, not routine flow.
|
|
70
|
+
log.warn("task_store_unavailable", { note: "pitasks store not initialized — taskId validation skipped" });
|
|
71
|
+
}
|
|
72
|
+
return store;
|
|
39
73
|
}
|
|
40
74
|
import {
|
|
41
75
|
decideAsyncLlmDispatch,
|
|
@@ -153,7 +187,7 @@ const SubagentParams = Type.Object({
|
|
|
153
187
|
Type.String({ description: "Display name for async agents in status line and notifications (e.g., 'Dream', 'Code Review'). Defaults to agent name." }),
|
|
154
188
|
),
|
|
155
189
|
taskId: Type.Optional(
|
|
156
|
-
Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\"
|
|
190
|
+
Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\" only for a single read-only check; an agent that edits, pushes, or drives a PR needs a real task id." }),
|
|
157
191
|
),
|
|
158
192
|
asyncKill: Type.Optional(
|
|
159
193
|
Type.String({ description: "Kill async agent(s) by name, id prefix, or 'all'. Returns which agents were killed." }),
|
|
@@ -577,22 +611,27 @@ export async function runSingleAgent(
|
|
|
577
611
|
}
|
|
578
612
|
}
|
|
579
613
|
|
|
580
|
-
/**
|
|
581
|
-
function
|
|
614
|
+
/** The message for a task id the store does not know; null when it is valid or exempt. */
|
|
615
|
+
export function taskIdError(taskId: string, lookup: (id: string) => boolean): string | null {
|
|
616
|
+
if (taskId === "-1") return null;
|
|
617
|
+
return lookup(taskId)
|
|
618
|
+
? null
|
|
619
|
+
: `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" for a read-only check (editing or PR work needs a real task id).`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Validate that a taskId references an existing task. Awaits the store load, so the
|
|
623
|
+
* first async call of a session validates too instead of failing open (issue #1016). */
|
|
624
|
+
async function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): Promise<string | null> {
|
|
582
625
|
if (taskId === "-1") return null;
|
|
626
|
+
const store = await getTaskStore();
|
|
627
|
+
if (!store) return null; // pitasks genuinely unavailable — warned in getTaskStore
|
|
583
628
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
// Store not loaded yet (async init) — fail open, skip validation
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
if (store.get(taskId)) return null;
|
|
594
|
-
} catch { return null; }
|
|
595
|
-
return `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" if not linked to a task.`;
|
|
629
|
+
return taskIdError(taskId, (id) => Boolean(store.get(id)));
|
|
630
|
+
} catch (err) {
|
|
631
|
+
// Same fail-open as before, but a broken store must not look like a valid id.
|
|
632
|
+
log.warn("task_id_validation_failed", { reason: String((err as Error)?.message ?? err).slice(0, 120) });
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
596
635
|
}
|
|
597
636
|
|
|
598
637
|
// ── Registration ─────────────────────────────────────────────────────────
|
|
@@ -629,7 +668,7 @@ export function registerSubagentTool(
|
|
|
629
668
|
};
|
|
630
669
|
}
|
|
631
670
|
|
|
632
|
-
function executeAsync(
|
|
671
|
+
async function executeAsync(
|
|
633
672
|
params: any,
|
|
634
673
|
agents: AgentConfig[],
|
|
635
674
|
mkd: (mode: "single" | "parallel" | "chain") => (results: SingleResult[]) => SubagentDetails,
|
|
@@ -659,13 +698,13 @@ export function registerSubagentTool(
|
|
|
659
698
|
}
|
|
660
699
|
const noTaskId = params.tasks.filter((t: any) => (t as any).taskId == null);
|
|
661
700
|
if (noTaskId.length > 0) {
|
|
662
|
-
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is
|
|
701
|
+
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)`;
|
|
663
702
|
// Force a follow-up turn so the AI MUST deal with this error
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
703
|
+
await sendSystemNote(
|
|
704
|
+
pi,
|
|
705
|
+
`🚨 CRITICAL: Async subagent call was REJECTED — missing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}. NO agents were spawned. You MUST retry with taskId for each agent. Use TaskList to find task IDs; "-1" is only for a single read-only check.`,
|
|
706
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
707
|
+
);
|
|
669
708
|
return {
|
|
670
709
|
content: [{ type: "text" as const, text: errorMsg }],
|
|
671
710
|
details: mkd("single")([]),
|
|
@@ -674,13 +713,13 @@ export function registerSubagentTool(
|
|
|
674
713
|
}
|
|
675
714
|
for (const t of params.tasks) {
|
|
676
715
|
const tid = (t as any).taskId as string;
|
|
677
|
-
const taskErr = validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
716
|
+
const taskErr = await validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
678
717
|
if (taskErr) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
718
|
+
await sendSystemNote(
|
|
719
|
+
pi,
|
|
720
|
+
`🚨 CRITICAL: ${taskErr} (agent: ${t.agent}). NO agents were spawned. Fix the taskId and retry.`,
|
|
721
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
722
|
+
);
|
|
684
723
|
return {
|
|
685
724
|
content: [{ type: "text" as const, text: `${taskErr} (agent: ${t.agent})` }],
|
|
686
725
|
details: mkd("single")([]),
|
|
@@ -747,13 +786,13 @@ export function registerSubagentTool(
|
|
|
747
786
|
};
|
|
748
787
|
}
|
|
749
788
|
if (params.taskId == null) {
|
|
750
|
-
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is
|
|
789
|
+
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)\n\nExample: subagent(agent="${params.agent}", task="...", async=true, name="${params.name}", taskId="-1")`;
|
|
751
790
|
// Force a follow-up turn so the AI MUST deal with this error
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
791
|
+
await sendSystemNote(
|
|
792
|
+
pi,
|
|
793
|
+
`🚨 CRITICAL: Async subagent call for "${params.agent}" was REJECTED — missing taskId. The agent was NOT spawned. You MUST retry the subagent call with taskId. Use TaskList to find the task ID; "-1" is only for a single read-only check. Do NOT proceed without fixing this.`,
|
|
794
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
795
|
+
);
|
|
757
796
|
return {
|
|
758
797
|
content: [{ type: "text" as const, text: errorMsg }],
|
|
759
798
|
details: mkd("single")([]),
|
|
@@ -761,13 +800,13 @@ export function registerSubagentTool(
|
|
|
761
800
|
};
|
|
762
801
|
}
|
|
763
802
|
{
|
|
764
|
-
const taskErr = validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
803
|
+
const taskErr = await validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
765
804
|
if (taskErr) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
805
|
+
await sendSystemNote(
|
|
806
|
+
pi,
|
|
807
|
+
`🚨 CRITICAL: ${taskErr} The agent was NOT spawned. Fix the taskId and retry.`,
|
|
808
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
809
|
+
);
|
|
771
810
|
return {
|
|
772
811
|
content: [{ type: "text" as const, text: taskErr }],
|
|
773
812
|
details: mkd("single")([]),
|
|
@@ -1689,7 +1728,7 @@ export function registerSubagentTool(
|
|
|
1689
1728
|
|
|
1690
1729
|
// Async mode — spawn in background and return immediately
|
|
1691
1730
|
if (params.async === true) {
|
|
1692
|
-
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1731
|
+
const result = await executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1693
1732
|
if (capabilityNote && result.content?.[0]?.type === "text") {
|
|
1694
1733
|
result.content[0].text = `${capabilityNote}\n\n${result.content[0].text}`;
|
|
1695
1734
|
}
|
|
@@ -11,6 +11,7 @@ import { TaskStore } from "./task-store.js";
|
|
|
11
11
|
import { TaskWidget } from "./task-widget.js";
|
|
12
12
|
import { registerTaskTools } from "./task-tools.js";
|
|
13
13
|
import { createLogger } from "../shared/logger.js";
|
|
14
|
+
import { publishTaskStore } from "../shared/task-store-bridge.js";
|
|
14
15
|
import { createBridgedUi } from "../shared/ui-bridge.js";
|
|
15
16
|
import { getSetting } from "../orchestrator/project-settings.js";
|
|
16
17
|
import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
|
|
@@ -43,6 +44,11 @@ export function setTaskTelemetryActive(taskId: string, active = true): void {
|
|
|
43
44
|
/** Exported store instance — set during extension init. */
|
|
44
45
|
export let taskStore: TaskStore;
|
|
45
46
|
|
|
47
|
+
// The store is published to other extension modules through the shared bridge: they
|
|
48
|
+
// cannot read this `export let` binding (they would snapshot `undefined`), which silently
|
|
49
|
+
// disabled task-id validation (issue #1016). Published once, from registration only —
|
|
50
|
+
// the bridge's getter follows later store swaps, so the upgrade path needs no republish.
|
|
51
|
+
|
|
46
52
|
/** Create a task directly — for use by other extensions (e.g., coms auto-create). */
|
|
47
53
|
export function createTask(subject: string, description: string, createdBy?: any, metadata?: Record<string, any>): any {
|
|
48
54
|
return taskStore.create(subject, description, createdBy || { type: "local", origin: "system", session: "", project: "" }, undefined, metadata);
|
|
@@ -189,6 +195,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
195
|
currentWidget?.dispose();
|
|
190
196
|
let store = new TaskStore(resolveStorePath());
|
|
191
197
|
taskStore = store;
|
|
198
|
+
// Registration is the only publish site: the getter follows store swaps.
|
|
199
|
+
publishTaskStore(() => taskStore);
|
|
192
200
|
currentStore = store;
|
|
193
201
|
const widget = new TaskWidget(store);
|
|
194
202
|
store.setOnChange(() => { widget.update(); broadcastTasks(); });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance-marked injections for content the user did NOT write.
|
|
3
|
+
*
|
|
4
|
+
* The transcript gives an extension no room to say "this is not the user's words":
|
|
5
|
+
* `pi.sendMessage` renders as `role: "custom"` and `pi.sendUserMessage` renders as a
|
|
6
|
+
* user turn, so machine text arrives looking exactly like something the user typed.
|
|
7
|
+
* A model reading it will answer "as you said" and attribute its own guard rails to the
|
|
8
|
+
* user (issue #1016 review, where a REJECTED call looked like a user message).
|
|
9
|
+
*
|
|
10
|
+
* Every message this module sends starts with MARKER, which is self-explanatory on
|
|
11
|
+
* purpose: no rule has to teach the model what it means.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { createLogger } from "./logger.js";
|
|
15
|
+
|
|
16
|
+
const log = createLogger("system-note");
|
|
17
|
+
|
|
18
|
+
/** Prefix that identifies machine-generated text in the transcript. */
|
|
19
|
+
export const SYSTEM_NOTE_MARKER = "[system message — not written by the user]";
|
|
20
|
+
|
|
21
|
+
export interface SystemNoteOptions {
|
|
22
|
+
/** Stable kind for renderers/filters, e.g. "subagent-taskid-error". */
|
|
23
|
+
customType: string;
|
|
24
|
+
/** Start a turn from this message when the session is idle. */
|
|
25
|
+
triggerTurn?: boolean;
|
|
26
|
+
/** Delivery slot for a busy session. */
|
|
27
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
28
|
+
/** Render in the TUI transcript. */
|
|
29
|
+
display?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Send machine-generated text, marked so it cannot be mistaken for user input. */
|
|
33
|
+
export async function sendSystemNote(
|
|
34
|
+
pi: ExtensionAPI,
|
|
35
|
+
text: string,
|
|
36
|
+
options: SystemNoteOptions,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const body = text.startsWith(SYSTEM_NOTE_MARKER) ? text : `${SYSTEM_NOTE_MARKER}\n${text}`;
|
|
39
|
+
log.debug("send_system_note", { customType: options.customType, chars: body.length });
|
|
40
|
+
await pi.sendMessage(
|
|
41
|
+
{ customType: options.customType, content: body, display: options.display ?? true },
|
|
42
|
+
{ triggerTurn: options.triggerTurn, deliverAs: options.deliverAs },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-module bridge for the pitasks store.
|
|
3
|
+
*
|
|
4
|
+
* `pitasks` exports `taskStore` as `export let`, assigned when the extension
|
|
5
|
+
* initializes. Another extension's module instance cannot read that binding: it
|
|
6
|
+
* snapshots `undefined` at import time, which silently disabled task-id validation
|
|
7
|
+
* (issue #1016). The store is therefore published on the global symbol registry, and
|
|
8
|
+
* the published object reads the live binding on every access — publishing once is
|
|
9
|
+
* enough, and a store swap needs no republish.
|
|
10
|
+
*
|
|
11
|
+
* Publish only from the instance that owns the store (pitasks registration), never from
|
|
12
|
+
* module-load code: a duplicate import would otherwise displace the real getter with one
|
|
13
|
+
* bound to a store that instance never creates.
|
|
14
|
+
*
|
|
15
|
+
* Same pattern as the pricing-provenance and session-cwd markers.
|
|
16
|
+
*/
|
|
17
|
+
import { createLogger } from "./logger.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("task-store-bridge");
|
|
20
|
+
|
|
21
|
+
export const TASK_STORE_SYMBOL = Symbol.for("pi-config.taskStore");
|
|
22
|
+
|
|
23
|
+
interface TaskStoreBridge {
|
|
24
|
+
readonly current: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Publish a getter over the live store binding. Called by the owning extension. */
|
|
28
|
+
export function publishTaskStore(getStore: () => unknown): void {
|
|
29
|
+
(globalThis as Record<symbol, unknown>)[TASK_STORE_SYMBOL] = {
|
|
30
|
+
get current(): unknown {
|
|
31
|
+
return getStore();
|
|
32
|
+
},
|
|
33
|
+
} satisfies TaskStoreBridge;
|
|
34
|
+
log.debug("task_store_bridge_published", { at: Date.now() });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The published store, or null when the owning extension has not registered. */
|
|
38
|
+
export function publishedTaskStore(): unknown {
|
|
39
|
+
const bridge = (globalThis as Record<symbol, unknown>)[TASK_STORE_SYMBOL] as TaskStoreBridge | undefined;
|
|
40
|
+
const store = bridge?.current ?? null;
|
|
41
|
+
log.debug("task_store_bridge_read", { found: Boolean(store) });
|
|
42
|
+
return store;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -34,7 +34,8 @@ Outside slash commands, never use edit, write, or bash directly (except `codemod
|
|
|
34
34
|
|
|
35
35
|
## Before Implementation (MANDATORY)
|
|
36
36
|
|
|
37
|
-
**For 2+ distinct actions, your first action is one `TaskBulkCreate` listing every step**
|
|
37
|
+
**For 2+ distinct actions — requested, or set in motion by an async result — your first action is one `TaskBulkCreate` listing every step**
|
|
38
|
+
— before the first edit or the first agent call (see `60-task-tracking.md`).
|
|
38
39
|
|
|
39
40
|
Before ANY code changes, run the pre-implementation checklist:
|
|
40
41
|
|
|
@@ -34,7 +34,7 @@ its output. Use it ONLY for work whose result nobody consumes — in pi-config,
|
|
|
34
34
|
dreaming and nothing else. Every watcher, poll, and review agent exists to report
|
|
35
35
|
something back, so leave `fireAndForget` unset for them. If you are about to set it,
|
|
36
36
|
ask whether you will ever read the result; if yes, do not set it.
|
|
37
|
-
**`taskId` is required on every async call** (
|
|
37
|
+
**`taskId` is required on every async call** (`-1` only for a single read-only check; anything that edits, pushes, or drives a PR needs a real task id) — a call without
|
|
38
38
|
it is rejected, and the agent does not run.
|
|
39
39
|
|
|
40
40
|
Do NOT write bash loops, sleep commands, or poll for results after spawning async agents.
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
## When Tasks Are Required
|
|
11
11
|
|
|
12
|
-
The request needs **2+ distinct actions** — then tasks are required:
|
|
12
|
+
The request — **or work any step sets in motion** — needs **2+ distinct actions** — then tasks are required:
|
|
13
13
|
|
|
14
14
|
- more than one file to change, or code plus tests plus docs
|
|
15
15
|
- any workflow from `05-issue-first-workflow.md` or `20-code-review-loop.md`
|
|
16
16
|
- the user lists several things to do ("do X, then Y", numbered items)
|
|
17
17
|
- work conditioned on a future event ("after Qodo approves, deploy and test") — create the tasks now, put the trigger in the description, and block the dependents on it
|
|
18
|
+
- an async result comes back with follow-up work — reacting only to finished agents still needs a board
|
|
18
19
|
|
|
19
20
|
**No tasks:** a single-step action, a one-file/one-line change, a question, an explanation, read-only investigation.
|
|
20
21
|
|
|
@@ -22,6 +23,10 @@ The request needs **2+ distinct actions** — then tasks are required:
|
|
|
22
23
|
|
|
23
24
|
Plan the whole job in **one `TaskBulkCreate` call** listing every step — not one `TaskCreate` per step, and not after the first edit. `TaskCreate` is for a single step added later.
|
|
24
25
|
|
|
26
|
+
**Before dispatching new work, reconcile the board (MANDATORY):** complete, re-scope, or
|
|
27
|
+
delete stale tasks. Dispatching over a task that is `in_progress` for work that already changed
|
|
28
|
+
is a violation.
|
|
29
|
+
|
|
25
30
|
When `TaskCreate`'s own tool description suggests per-step calls, **this rule wins**: one bulk call for the whole job.
|
|
26
31
|
|
|
27
32
|
## Granularity
|
|
@@ -31,10 +36,22 @@ One concrete action per task: which file, which change.
|
|
|
31
36
|
- ❌ `Implement the fix` / `Create PR`
|
|
32
37
|
- ✅ `Edit extensions/orchestrator/utils.ts — add timeout parameter to fetchData()` / `Create PR with the test plan`
|
|
33
38
|
|
|
39
|
+
**Multi-issue work:** one task per **stage** per issue, named `#<n>: <stage>` (e.g.
|
|
40
|
+
`#1016: contract test`). Later stages carry `blockedBy` in the bulk create — `addBlockedBy` is an
|
|
41
|
+
update field, and stage ids only exist after the call returns, so wire them with `TaskUpdate` if
|
|
42
|
+
the order was not set at creation. File-level detail lives in the description, not the subject.
|
|
43
|
+
|
|
34
44
|
## Lifecycle
|
|
35
45
|
|
|
36
46
|
`in_progress` before starting a task, `completed` immediately after. Work in order. Never abandon a task — drop a stale one with `TaskUpdate status: "deleted"`.
|
|
37
47
|
|
|
48
|
+
**When an async result arrives, update the board in that same turn:** on success complete the
|
|
49
|
+
owning task, then plan any follow-up in one `TaskBulkCreate` and activate its first stage. A
|
|
50
|
+
failed agent keeps its task open — the runtime never auto-completes a failure, and closing it
|
|
51
|
+
would activate a stage whose input never arrived.
|
|
52
|
+
|
|
53
|
+
Status reports come from `TaskList`, not recollection.
|
|
54
|
+
|
|
38
55
|
## Side Questions
|
|
39
56
|
|
|
40
57
|
Answer the question, then resume the next unchecked task — the task list says where you left off.
|
|
@@ -44,14 +61,18 @@ Answer the question, then resume the next unchecked task — the task list says
|
|
|
44
61
|
## Async Agent taskId (MANDATORY — code-enforced)
|
|
45
62
|
|
|
46
63
|
**Every async agent call MUST include `taskId`.** Enforced — calls without it are rejected.
|
|
47
|
-
Pass the task
|
|
48
|
-
|
|
64
|
+
Pass the id of the task the agent performs (e.g. `"5"`). `"-1"` is only for a single read-only
|
|
65
|
+
check that changes nothing: an agent that edits code, pushes, or drives a PR must carry a real
|
|
66
|
+
task id, because an unlinked poll is work that never appears on the board.
|
|
49
67
|
|
|
50
68
|
```text
|
|
51
69
|
subagent(agent="code-reviewer-quality", task="...", cwd="...", async=true, name="Review", taskId="5")
|
|
52
|
-
subagent(agent="worker", task="...", cwd="...", async=true, name="
|
|
70
|
+
subagent(agent="worker", task="...", cwd="...", async=true, name="Read one file", taskId="-1")
|
|
53
71
|
```
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
**Stage tasks are yours to close.** An agent links to the narrow task it performs; the stage
|
|
74
|
+
completes when the stage's acceptance holds — binding a stage to a poll marks it done when
|
|
75
|
+
only the poll finished.
|
|
76
|
+
|
|
77
|
+
- ❌ **NEVER** let a narrow agent task stand in for a stage — auto-complete ends the task the agent was linked to
|
|
57
78
|
- ❌ **NEVER** omit `taskId` — the call will fail
|