pi-orchestrator-config 4.22.3 → 4.22.4
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/agents/git-expert.md
CHANGED
|
@@ -44,11 +44,20 @@ Before ANY `git commit`, if `review_loop_enforcement` is `true`:
|
|
|
44
44
|
{{SETTINGS:dco,commit_trailer,use_worktrees,allow_push_to_protected_branches}}
|
|
45
45
|
|
|
46
46
|
- If `dco` is `true`: add `--signoff` to `git commit`. Skip if already present.
|
|
47
|
-
- If `commit_trailer` is a string (e.g. `"Assisted-by"`):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
- If `commit_trailer` is a string (e.g. `"Assisted-by"`): every commit you make MUST carry that
|
|
48
|
+
trailer, and who writes it depends on the commit shape:
|
|
49
|
+
- `git commit -m "…"` → the enforcement hook appends `TRAILER: PI (<model>)`.
|
|
50
|
+
Do not write it yourself.
|
|
51
|
+
- `echo … | git commit -F -` → the hook appends it. Do not write it yourself.
|
|
52
|
+
- `git commit -F <file>` (message written to a file, including heredoc-to-file) → the hook never
|
|
53
|
+
sees the file's contents, so append the trailer yourself with the already-resolved id:
|
|
54
|
+
`printf '\n\nTRAILER: PI (%s)\n' "$PI_MODEL" >> <file>` and then commit. The shell expands
|
|
55
|
+
`$PI_MODEL` while writing the file, so no placeholder can reach the message.
|
|
56
|
+
Never write a bare `MODEL` placeholder and never leave an unexpanded `$PI_MODEL` in a `-m` message:
|
|
57
|
+
the enforcement hook **refuses** such a commit (it reports the placeholder line back), and it also
|
|
58
|
+
refuses a commit shape it cannot inspect when the message shows no trailer. The fix is always to
|
|
59
|
+
write the trailer yourself with the real id, then commit again.
|
|
60
|
+
Skip only when the message already carries that trailer.
|
|
52
61
|
- If `use_worktrees` is `true`: BLOCK `git checkout` (except `git checkout -- <file>`) and `git switch`.
|
|
53
62
|
Use `git worktree add .worktrees/NAME -b BRANCH DEFAULT_BRANCH` instead
|
|
54
63
|
(detect default branch via `git symbolic-ref refs/remotes/origin/HEAD`, fallback to `main`).
|
|
@@ -2606,24 +2606,103 @@ function bodyAlreadySigned(text: string): boolean {
|
|
|
2606
2606
|
return false;
|
|
2607
2607
|
}
|
|
2608
2608
|
|
|
2609
|
-
// Trailer-shaped line: `Token:` at line start (real newline, literal \n
|
|
2610
|
-
//
|
|
2611
|
-
//
|
|
2612
|
-
//
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
/**
|
|
2616
|
-
*
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
* Scoped to `git commit` commands and to trailer-shaped lines only, so
|
|
2620
|
-
* commit-message prose that legitimately mentions `$PI_MODEL` — bare or
|
|
2621
|
-
* mid-sentence — and unrelated parts of compound commands are untouched. */
|
|
2622
|
-
export function fixUnexpandedCommitTrailer(command: string, modelId: string): string | null {
|
|
2623
|
-
if (!command.includes("$PI_MODEL")) return null;
|
|
2609
|
+
// Trailer-shaped line: `Token:` at line start (real newline, literal \n escape, or
|
|
2610
|
+
// command start), then a PI trailer identity that is a PLACEHOLDER rather than a model
|
|
2611
|
+
// id. Detection only — the hook refuses such a commit instead of rewriting text it may
|
|
2612
|
+
// have misparsed (Qodo, PR #1004).
|
|
2613
|
+
const placeholderTrailerLine = /(^|\n|\\n|-m\s+['"])([ \t]*[A-Za-z][A-Za-z0-9-]*:[ \t]*)PI \(\$?\{?PI_MODEL\}?|MODEL\)/g;
|
|
2614
|
+
|
|
2615
|
+
/** The trailer line carrying an unresolved placeholder, or null when the command has none.
|
|
2616
|
+
* A `Token:` anchor keeps prose that merely mentions the placeholder out of the result. */
|
|
2617
|
+
export function findPlaceholderTrailer(command: string): string | null {
|
|
2618
|
+
if (!/(?:PI_MODEL|\bMODEL\b)/.test(command)) return null;
|
|
2624
2619
|
if (!/\bgit\s+commit\b/.test(command)) return null;
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2620
|
+
const matches = [...command.matchAll(placeholderTrailerLine)];
|
|
2621
|
+
const found = matches.at(-1);
|
|
2622
|
+
if (!found) {
|
|
2623
|
+
enfLog.debug("placeholder trailer none");
|
|
2624
|
+
return null;
|
|
2625
|
+
}
|
|
2626
|
+
const line = `${(found[2] ?? "").trim()} PI (placeholder)`;
|
|
2627
|
+
enfLog.warn("placeholder trailer found", line);
|
|
2628
|
+
return line;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
/** Trailers where git itself needs an address: `Co-authored-by` credits a person. */
|
|
2632
|
+
const TRAILERS_REQUIRING_EMAIL = new Set(["co-authored-by"]);
|
|
2633
|
+
|
|
2634
|
+
/** The value appended after `<Trailer>:`. `Assisted-by` needs no address, `Co-authored-by`
|
|
2635
|
+
* does — hosting integrations only credit a co-author through `Name <email>`. */
|
|
2636
|
+
export function commitTrailerIdentity(trailerName: string, modelId: string): string {
|
|
2637
|
+
const needsEmail = TRAILERS_REQUIRING_EMAIL.has(trailerName.trim().toLowerCase());
|
|
2638
|
+
const identity = needsEmail ? `PI (${modelId}) <noreply@pi.dev>` : `PI (${modelId})`;
|
|
2639
|
+
enfLog.debug("commit_trailer_identity", trailerName, needsEmail);
|
|
2640
|
+
return identity;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/** Does a `-F <file>` commit message already carry the trailer?
|
|
2644
|
+
* The caller injects the reader so the decision is testable without touching disk —
|
|
2645
|
+
* the hook reads the file, it does not guess from the command text (Qodo, PR #1004). */
|
|
2646
|
+
export function messageFileTrailerState(
|
|
2647
|
+
filePath: string,
|
|
2648
|
+
trailerName: string,
|
|
2649
|
+
read: (path: string) => string,
|
|
2650
|
+
): "has" | "missing" | "unreadable" {
|
|
2651
|
+
try {
|
|
2652
|
+
const state = commandHasTrailerByName(read(filePath).slice(0, 64_000), trailerName) ? "has" : "missing";
|
|
2653
|
+
enfLog.debug("message_file_trailer", filePath, state);
|
|
2654
|
+
return state;
|
|
2655
|
+
} catch (error: any) {
|
|
2656
|
+
// warn, not debug: trailer verification could not inspect the message, which is a
|
|
2657
|
+
// degraded guarantee an operator needs to see (Qodo, PR #1004).
|
|
2658
|
+
enfLog.warn("message_file_trailer unreadable", filePath, String(error?.message ?? error));
|
|
2659
|
+
return "unreadable";
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/** Append `<Trailer>: PI (<model>)` to the message of a real commit invocation.
|
|
2664
|
+
* Covers the two shapes whose message the hook can see — `echo … | git commit -F -`
|
|
2665
|
+
* and `git commit -m "…"` — and returns null when the trailer is already there or
|
|
2666
|
+
* the shape is not covered. Pure so the injection path is testable without a hook. */
|
|
2667
|
+
export function injectCommitTrailer(command: string, trailerName: string, modelId: string): string | null {
|
|
2668
|
+
const identity = commitTrailerIdentity(trailerName, modelId);
|
|
2669
|
+
// Pattern A: echo "..." | git commit -F -
|
|
2670
|
+
const pipeIdx = command.lastIndexOf("|");
|
|
2671
|
+
if (pipeIdx !== -1 && /git\s+commit\s+.*-F\s*-/.test(command.slice(pipeIdx))) {
|
|
2672
|
+
const echoPart = command.slice(0, pipeIdx);
|
|
2673
|
+
const gitPart = command.slice(pipeIdx);
|
|
2674
|
+
const lastQuoteIdx = Math.max(echoPart.lastIndexOf('"'), echoPart.lastIndexOf("'"));
|
|
2675
|
+
if (lastQuoteIdx > 0) {
|
|
2676
|
+
if (commandHasTrailerByName(echoPart, trailerName)) {
|
|
2677
|
+
enfLog.debug("inject_commit_trailer", "already present", trailerName);
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
const quoteChar = echoPart[lastQuoteIdx];
|
|
2681
|
+
const escaped = quoteChar === "'"
|
|
2682
|
+
? `${escapeForSingleQuote(trailerName)}: ${identity}`
|
|
2683
|
+
: `${escapeForDoubleQuote(trailerName)}: ${identity}`;
|
|
2684
|
+
enfLog.info("inject_commit_trailer", "piped", trailerName);
|
|
2685
|
+
return echoPart.slice(0, lastQuoteIdx) + `\\n\\n${escaped.replace(/\n/g, "\\n")}` + echoPart.slice(lastQuoteIdx) + gitPart;
|
|
2686
|
+
}
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
// Pattern B: git commit -m "..." or git commit -m '...'
|
|
2690
|
+
const mFlagMatch = command.match(/git\s+commit\s+.*-m\s+(["'])([\s\S]*?)\1/);
|
|
2691
|
+
if (!mFlagMatch) return null;
|
|
2692
|
+
// Every `-m` is a message paragraph, so an existing trailer in ANY of them wins.
|
|
2693
|
+
const allPayloads = [...command.matchAll(/-m\s+(["'])([\s\S]*?)\1/g)].map((m) => m[2] ?? "").join("\n");
|
|
2694
|
+
if (commandHasTrailerByName(allPayloads, trailerName)) {
|
|
2695
|
+
enfLog.debug("inject_commit_trailer", "already present", trailerName);
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
const quoteChar = mFlagMatch[1];
|
|
2699
|
+
const escaped = quoteChar === "'"
|
|
2700
|
+
? `${escapeForSingleQuote(trailerName)}: ${identity}`
|
|
2701
|
+
: `${escapeForDoubleQuote(trailerName)}: ${identity}`;
|
|
2702
|
+
const fullMatch = mFlagMatch[0];
|
|
2703
|
+
const insertPos = command.indexOf(fullMatch) + fullMatch.length - 1;
|
|
2704
|
+
enfLog.info("inject_commit_trailer", "inline", trailerName);
|
|
2705
|
+
return command.slice(0, insertPos) + `\n\n${escaped}` + command.slice(insertPos);
|
|
2627
2706
|
}
|
|
2628
2707
|
|
|
2629
2708
|
/** Keep the first standalone valid footer, but use the current operation's model. */
|
|
@@ -20,7 +20,8 @@ import { getProjectTmpDir, resolveWorktreeRoot } from "./utils.js";
|
|
|
20
20
|
import {
|
|
21
21
|
conflictCandidateDirs,
|
|
22
22
|
pushTargetsProtectedBranch,
|
|
23
|
-
|
|
23
|
+
findPlaceholderTrailer,
|
|
24
|
+
messageFileTrailerState,
|
|
24
25
|
maskQuotedRegions,
|
|
25
26
|
isStdinFedShellStatement,
|
|
26
27
|
} from "./enforcement-helpers.js";
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
gitSubArgRegions,
|
|
54
55
|
hasGitAddBulk,
|
|
55
56
|
hasGitAddForce,
|
|
57
|
+
injectCommitTrailer,
|
|
56
58
|
injectSignoff,
|
|
57
59
|
injectGhBodySignature,
|
|
58
60
|
isGitIdentityConfigPath,
|
|
@@ -118,9 +120,23 @@ function writeRepeatState(state: { lastCmd: string; count: number }): void {
|
|
|
118
120
|
|
|
119
121
|
|
|
120
122
|
|
|
123
|
+
/** Placeholder strings that are never a real model id — printing one in a trailer ships a bug. */
|
|
124
|
+
const PLACEHOLDER_MODEL_IDS = new Set(["model", "pi_model", "$pi_model", "${pi_model}", "unknown"]);
|
|
125
|
+
|
|
121
126
|
/** Resolve the model id used in commit/PR trailers (env first, then ctx). */
|
|
122
|
-
function commitTrailerModelId(ctx: any): string {
|
|
123
|
-
|
|
127
|
+
export function commitTrailerModelId(ctx: any): string {
|
|
128
|
+
for (const raw of [process.env.PI_PRIMARY_MODEL, process.env.PI_MODEL, (ctx as any).model?.id]) {
|
|
129
|
+
const trimmed = (raw ?? "").trim();
|
|
130
|
+
if (!trimmed) continue;
|
|
131
|
+
if (PLACEHOLDER_MODEL_IDS.has(trimmed.toLowerCase())) {
|
|
132
|
+
log.debug("commit_trailer: skipped placeholder model id", trimmed);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const clean = trimmed.replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim();
|
|
136
|
+
if (clean) return clean;
|
|
137
|
+
}
|
|
138
|
+
log.warn("commit_trailer: no model id resolved — trailer identity is unknown");
|
|
139
|
+
return "unknown";
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
/** Inject commit trailer into command (Assisted-by, Co-authored-by, etc.) */
|
|
@@ -128,18 +144,20 @@ function commitTrailerModelId(ctx: any): string {
|
|
|
128
144
|
let bridgedUi: ReturnType<typeof createBridgedUi> | null = null;
|
|
129
145
|
|
|
130
146
|
async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd: string): Promise<EnforcementResult> {
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
// A placeholder trailer is REFUSED, never rewritten: the hook cannot safely tell
|
|
148
|
+
// command text from message prose (a commit message quoting the very shape it
|
|
149
|
+
// parses, a `;` inside quotes, indented examples), and rewriting the wrong span
|
|
150
|
+
// corrupts a message or edits an unrelated command. The agent has the text, so it
|
|
151
|
+
// fixes it and retries (Qodo, PR #1004).
|
|
152
|
+
const placeholder = findPlaceholderTrailer(command);
|
|
153
|
+
if (placeholder) {
|
|
154
|
+
return {
|
|
155
|
+
block: true,
|
|
156
|
+
reason:
|
|
157
|
+
`⛔ Commit refused: the trailer carries an unresolved placeholder (${placeholder}). ` +
|
|
158
|
+
`Write \`TRAILER: PI (${commitTrailerModelId(ctx)})\` instead of a MODEL/$PI_MODEL placeholder ` +
|
|
159
|
+
"and re-run the commit.",
|
|
160
|
+
};
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
const trailerSetting = getSetting(gitCwd, "commit_trailer");
|
|
@@ -147,7 +165,6 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
|
|
|
147
165
|
|
|
148
166
|
const modelId = commitTrailerModelId(ctx);
|
|
149
167
|
|
|
150
|
-
const piIdentity = `PI (${modelId}) <noreply@pi.dev>`;
|
|
151
168
|
let trailerName: string;
|
|
152
169
|
|
|
153
170
|
if (trailerSetting.includes(",")) {
|
|
@@ -184,55 +201,54 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
|
|
|
184
201
|
trailerName = trailerSetting;
|
|
185
202
|
}
|
|
186
203
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const gitPart = command.slice(pipeIdx);
|
|
192
|
-
const lastDoubleQuote = echoPart.lastIndexOf('"');
|
|
193
|
-
const lastSingleQuote = echoPart.lastIndexOf("'");
|
|
194
|
-
const lastQuoteIdx = Math.max(lastDoubleQuote, lastSingleQuote);
|
|
195
|
-
if (lastQuoteIdx > 0) {
|
|
196
|
-
// Determine quote context for correct escaping
|
|
197
|
-
const quoteChar = echoPart[lastQuoteIdx];
|
|
198
|
-
// Scope dedup to the echo/message-construction portion (before the pipe), which
|
|
199
|
-
// excludes the git args. Do NOT attempt to slice out the exact quoted substring
|
|
200
|
-
// by backward-scanning for a quote — escaped quotes (\") inside the message would
|
|
201
|
-
// misparse the boundary and cause a missed trailer -> duplicate injection.
|
|
202
|
-
if (commandHasTrailerByName(echoPart, trailerName)) {
|
|
203
|
-
log.debug("commit_trailer already present in echo message (by name) — skipping injection", trailerName);
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
const escaped = quoteChar === "'"
|
|
207
|
-
? `${escapeForSingleQuote(trailerName)}: ${piIdentity}`
|
|
208
|
-
: `${escapeForDoubleQuote(trailerName)}: ${piIdentity}`;
|
|
209
|
-
event.input.command =
|
|
210
|
-
echoPart.slice(0, lastQuoteIdx) + `\\n\\n${escaped.replace(/\n/g, "\\n")}` + echoPart.slice(lastQuoteIdx) + gitPart;
|
|
211
|
-
}
|
|
204
|
+
const injected = injectCommitTrailer(command, trailerName, modelId);
|
|
205
|
+
if (injected !== null) {
|
|
206
|
+
event.input.command = injected;
|
|
207
|
+
return undefined;
|
|
212
208
|
}
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
209
|
+
// `git commit -F <file>`: the message is a file we CAN read, so check it instead of
|
|
210
|
+
// guessing. Refusing a message that already carries the trailer would block the very
|
|
211
|
+
// flow the git-expert instruction describes (Qodo, PR #1004).
|
|
212
|
+
const fileMessage = command.match(/git\s+commit\b[^\n]*?-F\s+(?!-)(["']?)([^\s"']+)\1/);
|
|
213
|
+
if (fileMessage && fileMessage[2]) {
|
|
214
|
+
// The read happens BEFORE the command runs, so a command that writes the same file
|
|
215
|
+
// would be verified against its OLD contents while git commits the new ones. Refuse
|
|
216
|
+
// that shape instead of trusting the stale read (Qodo, PR #1004).
|
|
217
|
+
const filePathSrc = fileMessage[2].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
218
|
+
if (bashWritesToPath(command, maskQuotedRegions(command), filePathSrc)) {
|
|
219
|
+
log.warn("commit_trailer: message file is written by the same command", fileMessage[2]);
|
|
220
|
+
return {
|
|
221
|
+
block: true,
|
|
222
|
+
reason:
|
|
223
|
+
`⛔ Commit refused: this command writes ${fileMessage[2]} and commits it in the same call, ` +
|
|
224
|
+
"so the message cannot be verified before it is read. Write the message (with the trailer) in one call and commit in the next.",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const candidate = path.isAbsolute(fileMessage[2]) ? fileMessage[2] : join(gitCwd, fileMessage[2]);
|
|
228
|
+
const state = messageFileTrailerState(candidate, trailerName, (p) => readFileSync(p, "utf8"));
|
|
229
|
+
if (state === "has") {
|
|
230
|
+
log.debug("commit_trailer: message file already carries the trailer", candidate);
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
if (state === "missing") {
|
|
234
|
+
return {
|
|
235
|
+
block: true,
|
|
236
|
+
reason:
|
|
237
|
+
`⛔ Commit refused: the message file ${fileMessage[2]} has no \`${trailerName}\` trailer. ` +
|
|
238
|
+
`Append it with the real id — \`printf '\\n\\n" + trailerName + ": PI (%s)\\n' \"$PI_MODEL\" >> " + fileMessage[2] + "\` — then re-run the commit.`,
|
|
239
|
+
};
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
|
-
|
|
242
|
+
// Other opaque shapes must carry the trailer already; refusing beats guessing.
|
|
243
|
+
if (!commandHasTrailerByName(command, trailerName)) {
|
|
244
|
+
return {
|
|
245
|
+
block: true,
|
|
246
|
+
reason:
|
|
247
|
+
`⛔ Commit refused: no \`${trailerName}\` trailer found and this commit shape is opaque to the hook ` +
|
|
248
|
+
`(the message is built in a way it cannot inspect). Add \`${trailerName}: PI (${modelId})\` to the message ` +
|
|
249
|
+
"yourself — e.g. `printf '\\n\\n" + trailerName + ": PI (%s)\\n' \"$PI_MODEL\" >> <file>` for `-F <file>` — then re-run the commit.",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
236
252
|
return undefined;
|
|
237
253
|
}
|
|
238
254
|
|
package/package.json
CHANGED
|
@@ -34,6 +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** — before the first edit or the first agent call (see `60-task-tracking.md`).
|
|
38
|
+
|
|
37
39
|
Before ANY code changes, run the pre-implementation checklist:
|
|
38
40
|
|
|
39
41
|
→ **See the "Pre-Implementation Checklist" section below** — Do NOT skip this step.
|
|
@@ -7,58 +7,37 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## When
|
|
10
|
+
## When Tasks Are Required
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The request needs **2+ distinct actions** — then tasks are required:
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
- more than one file to change, or code plus tests plus docs
|
|
15
|
+
- any workflow from `05-issue-first-workflow.md` or `20-code-review-loop.md`
|
|
16
|
+
- the user lists several things to do ("do X, then Y", numbered items)
|
|
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
|
|
17
18
|
|
|
18
|
-
**
|
|
19
|
-
conditional on a future event ("after X", "once Y", "when Z"), create the
|
|
20
|
-
tasks IMMEDIATELY in that same turn — the trigger condition goes in the task
|
|
21
|
-
description, and dependent tasks block on the trigger task. Never carry a
|
|
22
|
-
deferred mission only in conversation memory: compaction or a session
|
|
23
|
-
restart loses it silently.
|
|
19
|
+
**No tasks:** a single-step action, a one-file/one-line change, a question, an explanation, read-only investigation.
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
- ✅ GOOD: `TaskCreate("Deploy X locally — trigger: Qodo R6 clean")` + blocked follow-ups
|
|
21
|
+
## First Action
|
|
27
22
|
|
|
28
|
-
**
|
|
23
|
+
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.
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Task Granularity
|
|
25
|
+
When `TaskCreate`'s own tool description suggests per-step calls, **this rule wins**: one bulk call for the whole job.
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
## Granularity
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
- ✅ GOOD: `Edit extensions/orchestrator/utils.ts — add timeout parameter to fetchData()` / `Run code review loop (3 async reviewers)` / `Create PR with description`
|
|
29
|
+
One concrete action per task: which file, which change.
|
|
38
30
|
|
|
39
|
-
|
|
31
|
+
- ❌ `Implement the fix` / `Create PR`
|
|
32
|
+
- ✅ `Edit extensions/orchestrator/utils.ts — add timeout parameter to fetchData()` / `Create PR with the test plan`
|
|
40
33
|
|
|
41
|
-
##
|
|
34
|
+
## Lifecycle
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
Mark each `in_progress` before starting and `completed` immediately after finishing via `TaskUpdate`.
|
|
45
|
-
Work through tasks in order — do not skip tasks or start new work while unchecked tasks exist (unless user explicitly pivots).
|
|
46
|
-
|
|
47
|
-
---
|
|
36
|
+
`in_progress` before starting a task, `completed` immediately after. Work in order. Never abandon a task — drop a stale one with `TaskUpdate status: "deleted"`.
|
|
48
37
|
|
|
49
38
|
## Side Questions
|
|
50
39
|
|
|
51
|
-
Answer the question, then resume the next unchecked task
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Integration & Key Rules
|
|
56
|
-
|
|
57
|
-
Task tracking works alongside existing workflow rules — include issue-first, code review, and documentation steps as individual tasks.
|
|
58
|
-
{{IF:task_reminder_enabled}}
|
|
59
|
-
Tasks are code-enforced (reminders after 4+ ignored turns).
|
|
60
|
-
{{/IF}}
|
|
61
|
-
Never abandon tasks — if scope changes, use `TaskUpdate` with `status: "deleted"` to remove obsolete tasks; the task list is your contract.
|
|
40
|
+
Answer the question, then resume the next unchecked task — the task list says where you left off.
|
|
62
41
|
|
|
63
42
|
---
|
|
64
43
|
|