@deftai/directive-core 0.95.0 → 0.96.0

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.
Files changed (47) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +630 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cache/main.js +298 -1
  6. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  7. package/dist/content-contracts/skills/helpers.js +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/init-deposit/hygiene.d.ts +1 -1
  11. package/dist/init-deposit/hygiene.js +16 -4
  12. package/dist/init-deposit/scaffold.js +6 -5
  13. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  14. package/dist/parent-turn-shape/evaluate.js +353 -0
  15. package/dist/parent-turn-shape/index.d.ts +8 -0
  16. package/dist/parent-turn-shape/index.js +8 -0
  17. package/dist/review-monitor/constants.js +3 -2
  18. package/dist/review-monitor/tier-detection.d.ts +7 -3
  19. package/dist/review-monitor/tier-detection.js +18 -1
  20. package/dist/review-monitor/verify.js +18 -0
  21. package/dist/scope/index.d.ts +2 -0
  22. package/dist/scope/index.js +2 -0
  23. package/dist/scope/main.d.ts +10 -0
  24. package/dist/scope/main.js +109 -24
  25. package/dist/scope/promote-from-issue.d.ts +49 -0
  26. package/dist/scope/promote-from-issue.js +367 -0
  27. package/dist/scope/promote-path.d.ts +39 -0
  28. package/dist/scope/promote-path.js +105 -0
  29. package/dist/swarm/routing.d.ts +4 -2
  30. package/dist/swarm/routing.js +26 -4
  31. package/dist/triage/actions/index.js +62 -2
  32. package/dist/triage/actions/types.d.ts +8 -1
  33. package/dist/triage/author-filter.d.ts +51 -0
  34. package/dist/triage/author-filter.js +152 -0
  35. package/dist/triage/classify/index.d.ts +2 -2
  36. package/dist/triage/classify/index.js +2 -2
  37. package/dist/triage/classify/label-mirror.d.ts +68 -5
  38. package/dist/triage/classify/label-mirror.js +261 -31
  39. package/dist/triage/help/registry-data.d.ts +49 -38
  40. package/dist/triage/help/registry-data.js +115 -40
  41. package/dist/triage/index.d.ts +1 -0
  42. package/dist/triage/index.js +1 -0
  43. package/dist/triage/queue/index.d.ts +1 -0
  44. package/dist/triage/queue/index.js +1 -0
  45. package/dist/triage/queue/render.d.ts +2 -0
  46. package/dist/triage/queue/render.js +6 -0
  47. package/package.json +7 -3
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Parent turn-shape hard-stop for text-repetition hang (FC14 / #3131).
3
+ *
4
+ * Soft skill prose (#2943) alone is insufficient: parents still burn the full
5
+ * output budget on identical progress sentences with zero tool_use after leaf
6
+ * announce. This pure library is the Directive-side machine-checkable gate.
7
+ *
8
+ * Hosts (OpenClaw, swarm parents, review monitors) SHOULD evaluate the parent
9
+ * turn stream mid-generation and hard-stop when `ok` is false.
10
+ *
11
+ * Illegal shape (MUST NOT):
12
+ * N > maxIdenticalWithoutTool (default 2) near-identical assistant sentences
13
+ * (or streaming text chunks) in one turn with no tool_use and no yield.
14
+ *
15
+ * Legal post-announce shapes:
16
+ * 1. Tool batch (ground-truth) then one consolidate
17
+ * 2. sessions_yield / wait without filler
18
+ * 3. One short user answer that is NOT a repeated progress line
19
+ */
20
+ /** Canonical fail class for the hang (#3131 / soft FC14 recurrence of #2943). */
21
+ export const PARENT_TURN_FAIL_FC14 = "FC14";
22
+ /** Default max identical text units without tool/yield (N>2 is illegal). */
23
+ export const DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL = 2;
24
+ /** Minimum length (chars) for a text unit to count toward repetition. */
25
+ const MIN_UNIT_LEN = 12;
26
+ /** Word Jaccard threshold for near-identical (after normalize). */
27
+ const JACCARD_NEAR = 0.9;
28
+ /** Progress-ish tokens that mark filler narration after announce. */
29
+ const PROGRESS_HINT_RE = /\b(checking|checking\s+worktrees|open\s+prs?|next|unfinished|implementing|looking\s+at|will\s+(check|inspect|verify|spawn)|status\s+next|monitor(?:ing)?)\b/i;
30
+ /**
31
+ * Normalize assistant text for identity comparison.
32
+ * Collapses whitespace, lowercases, strips common trailing punctuation.
33
+ */
34
+ export function normalizeTurnText(text) {
35
+ return text
36
+ .normalize("NFKC")
37
+ .toLowerCase()
38
+ .replace(/\s+/g, " ")
39
+ .replace(/^[\s"'`]+|[\s"'`]+$/g, "")
40
+ .replace(/[.!?…,:;]+$/g, "")
41
+ .trim();
42
+ }
43
+ /** Split a blob into sentence-like units (periods, newlines, ellipsis). */
44
+ export function splitTextUnits(text) {
45
+ const raw = text.replace(/\r\n/g, "\n");
46
+ // Include single newlines so block-formatted repeated progress lines still
47
+ // unitize (FC14 hang class often streams one line per newline, no period).
48
+ // Optional closing quotes after terminal punctuation, with or without space:
49
+ // `"Hello." "Hello."` and `"Hello.""Hello."`.
50
+ const parts = raw
51
+ .split(/(?:\n+|(?<=[.!?…])["']+\s*|(?<=[.!?…])\s+|(?<=\.\.\.)\s+)/)
52
+ .map((p) => p.trim())
53
+ .filter((p) => p.length > 0);
54
+ if (parts.length === 0 && raw.trim().length > 0) {
55
+ return [raw.trim()];
56
+ }
57
+ return parts;
58
+ }
59
+ function wordSet(normalized) {
60
+ const words = normalized.split(/[^a-z0-9_]+/).filter((w) => w.length > 0);
61
+ return new Set(words);
62
+ }
63
+ /**
64
+ * True when two normalized strings are near-identical (exact, containment with
65
+ * high length ratio, or high word Jaccard). Short units never match.
66
+ */
67
+ export function isNearIdentical(a, b) {
68
+ const na = normalizeTurnText(a);
69
+ const nb = normalizeTurnText(b);
70
+ if (na.length < MIN_UNIT_LEN || nb.length < MIN_UNIT_LEN)
71
+ return false;
72
+ if (na === nb)
73
+ return true;
74
+ const shorter = na.length <= nb.length ? na : nb;
75
+ const longer = na.length <= nb.length ? nb : na;
76
+ if (longer.includes(shorter) && shorter.length / longer.length >= 0.85) {
77
+ return true;
78
+ }
79
+ const sa = wordSet(na);
80
+ const sb = wordSet(nb);
81
+ if (sa.size === 0 || sb.size === 0)
82
+ return false;
83
+ let inter = 0;
84
+ for (const w of sa) {
85
+ if (sb.has(w))
86
+ inter += 1;
87
+ }
88
+ const union = sa.size + sb.size - inter;
89
+ if (union === 0)
90
+ return false;
91
+ return inter / union >= JACCARD_NEAR;
92
+ }
93
+ /**
94
+ * Collect assistant text units from turn events.
95
+ *
96
+ * Streaming hosts often emit word/chunk deltas as separate `assistant_text`
97
+ * events. Coalesce consecutive text events into one blob so fragmented
98
+ * progress sentences reconstruct before split/identity checks (P1 / #3131).
99
+ * Non-text events (tool_use / yield) flush the coalesce buffer.
100
+ */
101
+ function collectAssistantUnits(events) {
102
+ const units = [];
103
+ const pushUnitsFrom = (text) => {
104
+ for (const u of splitTextUnits(text)) {
105
+ const t = u.trim();
106
+ if (t.length >= MIN_UNIT_LEN)
107
+ units.push(t);
108
+ }
109
+ };
110
+ let run = "";
111
+ for (const ev of events) {
112
+ if (ev.kind === "assistant_text") {
113
+ const text = typeof ev.text === "string" ? ev.text : "";
114
+ if (!text)
115
+ continue;
116
+ // Sentence-boundary deltas often omit whitespace after `.!?` before the
117
+ // next capital letter. Insert space only at sentence ends — never between
118
+ // arbitrary alnum chunks (subword streams like workt+rees must not gain
119
+ // a false space).
120
+ if (run.length > 0 && /[.!?…]["']?\s*$/.test(run) && !/^\s/.test(text) && !/\s$/.test(run)) {
121
+ run += " ";
122
+ }
123
+ run += text;
124
+ continue;
125
+ }
126
+ // Non-text event ends the coalesce run (tool/yield boundaries).
127
+ if (run.trim().length > 0)
128
+ pushUnitsFrom(run);
129
+ run = "";
130
+ }
131
+ if (run.trim().length > 0)
132
+ pushUnitsFrom(run);
133
+ return units;
134
+ }
135
+ /**
136
+ * Max connected-component size under near-identity edges (union-find).
137
+ * Captures non-transitive chains of incrementally varied progress lines
138
+ * (A≈B≈C even when A≉C) so FC14 cannot be bypassed by wording drift.
139
+ */
140
+ function maxNearIdentityComponent(units) {
141
+ const n = units.length;
142
+ if (n === 0)
143
+ return 0;
144
+ // Precompute normalize + word sets once (avoid O(n²) re-normalize/Set alloc).
145
+ const norms = units.map((u) => normalizeTurnText(u));
146
+ const sets = norms.map((n) => wordSet(n));
147
+ const near = (i, j) => {
148
+ const na = norms[i] ?? "";
149
+ const nb = norms[j] ?? "";
150
+ if (na.length < MIN_UNIT_LEN || nb.length < MIN_UNIT_LEN)
151
+ return false;
152
+ if (na === nb)
153
+ return true;
154
+ const shorter = na.length <= nb.length ? na : nb;
155
+ const longer = na.length <= nb.length ? nb : na;
156
+ if (longer.includes(shorter) && shorter.length / longer.length >= 0.85)
157
+ return true;
158
+ const sa = sets[i] ?? new Set();
159
+ const sb = sets[j] ?? new Set();
160
+ if (sa.size === 0 || sb.size === 0)
161
+ return false;
162
+ let inter = 0;
163
+ for (const w of sa) {
164
+ if (sb.has(w))
165
+ inter += 1;
166
+ }
167
+ const union = sa.size + sb.size - inter;
168
+ return union > 0 && inter / union >= JACCARD_NEAR;
169
+ };
170
+ const parent = Array.from({ length: n }, (_, i) => i);
171
+ const find = (i) => {
172
+ let x = i;
173
+ while (parent[x] !== x) {
174
+ parent[x] = parent[parent[x] ?? x] ?? x;
175
+ x = parent[x] ?? x;
176
+ }
177
+ return x;
178
+ };
179
+ const unite = (a, b) => {
180
+ const ra = find(a);
181
+ const rb = find(b);
182
+ if (ra !== rb)
183
+ parent[ra] = rb;
184
+ };
185
+ for (let i = 0; i < n; i++) {
186
+ for (let j = i + 1; j < n; j++) {
187
+ if (near(i, j))
188
+ unite(i, j);
189
+ }
190
+ }
191
+ const sizes = new Map();
192
+ let max = 1;
193
+ for (let i = 0; i < n; i++) {
194
+ const r = find(i);
195
+ const next = (sizes.get(r) ?? 0) + 1;
196
+ sizes.set(r, next);
197
+ if (next > max)
198
+ max = next;
199
+ }
200
+ return max;
201
+ }
202
+ /**
203
+ * Max count of any single near-identical cluster among units.
204
+ * Consecutive runs + exact-normalized frequency (O(n)); for small unit counts
205
+ * also union-find near-identity components so non-consecutive / chain variants
206
+ * cannot bypass FC14 (P1 / #3131).
207
+ */
208
+ function maxIdenticalCluster(units) {
209
+ if (units.length === 0)
210
+ return 0;
211
+ const norms = units.map((u) => normalizeTurnText(u));
212
+ let maxRun = 1;
213
+ let run = 1;
214
+ for (let i = 1; i < units.length; i++) {
215
+ if (isNearIdentical(units[i] ?? "", units[i - 1] ?? "")) {
216
+ run += 1;
217
+ if (run > maxRun)
218
+ maxRun = run;
219
+ }
220
+ else {
221
+ run = 1;
222
+ }
223
+ }
224
+ // Frequency by exact normalized form (O(n)).
225
+ const freq = new Map();
226
+ let maxFreq = 1;
227
+ for (const n of norms) {
228
+ if (n.length < MIN_UNIT_LEN)
229
+ continue;
230
+ const next = (freq.get(n) ?? 0) + 1;
231
+ freq.set(n, next);
232
+ if (next > maxFreq)
233
+ maxFreq = next;
234
+ }
235
+ // Always cluster near-identity components. Input size is bounded by the
236
+ // zero-tool character hard-stop above, so O(n²) stays finite.
237
+ const maxComponent = maxNearIdentityComponent(units);
238
+ return Math.max(maxRun, maxFreq, maxComponent);
239
+ }
240
+ /**
241
+ * Detect when a single blob itself embeds the same sentence many times
242
+ * (model streams one giant assistant message of repeated lines).
243
+ */
244
+ export function countRepeatedUnitsInBlob(text) {
245
+ const units = splitTextUnits(text).filter((u) => u.trim().length >= MIN_UNIT_LEN);
246
+ return maxIdenticalCluster(units);
247
+ }
248
+ function looksLikeProgressOnly(units) {
249
+ if (units.length < 2)
250
+ return false;
251
+ let progressHits = 0;
252
+ for (const u of units) {
253
+ if (PROGRESS_HINT_RE.test(u))
254
+ progressHits += 1;
255
+ }
256
+ // Majority of multi-sentence text is progress narration.
257
+ return progressHits >= Math.ceil(units.length / 2);
258
+ }
259
+ /**
260
+ * Evaluate whether a parent turn shape is legal under the FC14 hard-stop.
261
+ * Pure / side-effect free — safe for mid-stream host gates and unit tests.
262
+ */
263
+ export function evaluateParentTurnShape(input) {
264
+ const events = input.events ?? [];
265
+ const maxAllowed = input.maxIdenticalWithoutTool ?? DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL;
266
+ let hasToolUse = false;
267
+ let hasYield = false;
268
+ for (const ev of events) {
269
+ if (ev.kind === "tool_use")
270
+ hasToolUse = true;
271
+ if (ev.kind === "yield")
272
+ hasYield = true;
273
+ }
274
+ // Tool or yield greases the turn: repetition hard-stop does not fire.
275
+ if (hasToolUse || hasYield) {
276
+ return {
277
+ ok: true,
278
+ failClass: "none",
279
+ reasons: [],
280
+ maxIdenticalCount: 0,
281
+ hasToolUse,
282
+ hasYield,
283
+ };
284
+ }
285
+ // Fail closed on unbounded zero-tool text (output-budget burn / DoS class).
286
+ const ZERO_TOOL_CHAR_CAP = 50_000;
287
+ let zeroToolChars = 0;
288
+ for (const ev of events) {
289
+ if (ev.kind === "assistant_text" && typeof ev.text === "string") {
290
+ zeroToolChars += ev.text.length;
291
+ }
292
+ }
293
+ if (zeroToolChars > ZERO_TOOL_CHAR_CAP) {
294
+ return {
295
+ ok: false,
296
+ failClass: "FC14",
297
+ reasons: [
298
+ `FC14 text-repetition-hang: zero-tool assistant text exceeds ${ZERO_TOOL_CHAR_CAP} chars ` +
299
+ `(${zeroToolChars}) without tool_use/yield — hard-stop budget burn. Refs #3131 / #2943.`,
300
+ ],
301
+ maxIdenticalCount: Math.ceil(zeroToolChars / MIN_UNIT_LEN),
302
+ hasToolUse,
303
+ hasYield,
304
+ };
305
+ }
306
+ const units = collectAssistantUnits(events);
307
+ // Also fold in whole-blob repetition for single giant text events.
308
+ let maxIdenticalCount = maxIdenticalCluster(units);
309
+ for (const ev of events) {
310
+ if (ev.kind === "assistant_text" && typeof ev.text === "string") {
311
+ maxIdenticalCount = Math.max(maxIdenticalCount, countRepeatedUnitsInBlob(ev.text));
312
+ }
313
+ }
314
+ const reasons = [];
315
+ let failClass = "none";
316
+ if (maxIdenticalCount > maxAllowed) {
317
+ failClass = "FC14";
318
+ reasons.push(`FC14 text-repetition-hang: ${maxIdenticalCount} near-identical assistant text units ` +
319
+ `with zero tool_use/yield (max allowed ${maxAllowed}; N>${maxAllowed} is illegal). ` +
320
+ `After subagent announce, emit a tool-first ground-truth batch, sessions_yield, ` +
321
+ `or one short non-repeated answer — not repeated progress lines. Refs #3131 / #2943.`);
322
+ }
323
+ // Post-announce: multi-sentence progress-only with zero tools is also illegal
324
+ // even when sentences are not exact clones (soft-only #2943 recurrence class).
325
+ // Threshold is >=2 units (N>1 multi-sentence) — exactly two progress lines
326
+ // must not bypass the gate.
327
+ if (input.afterSubagentAnnounce &&
328
+ units.length >= 2 &&
329
+ looksLikeProgressOnly(units) &&
330
+ failClass === "none") {
331
+ failClass = "progress-only-no-tool";
332
+ reasons.push(`progress-only-no-tool: post-subagent-announce turn has ${units.length} assistant ` +
333
+ `text units with progress narration and zero tool_use/yield. MUST tool-first or yield. Refs #3131 / #2943.`);
334
+ }
335
+ // Alias surface: FC14 and text-repetition-hang name the same hang class.
336
+ if (failClass === "FC14") {
337
+ // Keep failClass as FC14; callers may also match text-repetition-hang via reasons.
338
+ }
339
+ return {
340
+ ok: failClass === "none",
341
+ failClass,
342
+ reasons,
343
+ maxIdenticalCount,
344
+ hasToolUse,
345
+ hasYield,
346
+ };
347
+ }
348
+ /** Convenience: true when the turn is an illegal text-repetition hang. */
349
+ export function isTextRepetitionHang(input) {
350
+ const r = evaluateParentTurnShape(input);
351
+ return r.failClass === "FC14" || r.failClass === "text-repetition-hang";
352
+ }
353
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parent turn-shape hard-stop (FC14 / #3131).
3
+ *
4
+ * Machine-checkable gate so soft skill prose is not the sole mitigation for
5
+ * the OpenClaw parent text-repetition hang after leaf announce.
6
+ */
7
+ export { countRepeatedUnitsInBlob, DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL, evaluateParentTurnShape, isNearIdentical, isTextRepetitionHang, normalizeTurnText, PARENT_TURN_FAIL_FC14, type ParentTurnEvent, type ParentTurnFailClass, type ParentTurnShapeInput, type ParentTurnShapeResult, splitTextUnits, } from "./evaluate.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parent turn-shape hard-stop (FC14 / #3131).
3
+ *
4
+ * Machine-checkable gate so soft skill prose is not the sole mitigation for
5
+ * the OpenClaw parent text-repetition hang after leaf announce.
6
+ */
7
+ export { countRepeatedUnitsInBlob, DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL, evaluateParentTurnShape, isNearIdentical, isTextRepetitionHang, normalizeTurnText, PARENT_TURN_FAIL_FC14, splitTextUnits, } from "./evaluate.js";
8
+ //# sourceMappingURL=index.js.map
@@ -40,7 +40,7 @@ export const REVIEW_MONITOR_HELP = "usage: task verify:review-monitor -- --pr <N
40
40
  "\n" +
41
41
  "Claim a lease after spawning Approach 1:\n" +
42
42
  " task review-monitor:register -- --pr <N> --monitor-agent-id <id> \\\n" +
43
- " --platform-primitive cursor-task|spawn_subagent|start_agent|sessions_spawn \\\n" +
43
+ " --platform-primitive cursor-task|claude-agent|spawn_subagent|start_agent|sessions_spawn \\\n" +
44
44
  " [--head-sha SHA] [--repo OWNER/REPO] [--force]\n" +
45
45
  "\n" +
46
46
  "Release when done:\n" +
@@ -55,7 +55,8 @@ export const REGISTER_HELP = "usage: task review-monitor:register -- --pr <N> --
55
55
  " --pr N Pull request number\n" +
56
56
  " --monitor-agent-id ID Stable poller agent id / Task handle\n" +
57
57
  " --platform-primitive P start_agent | spawn_subagent | cursor-task |\n" +
58
- " sessions_spawn | openclaw-sessions-spawn (#2876)\n" +
58
+ " claude-agent (#3134) | sessions_spawn |\n" +
59
+ " openclaw-sessions-spawn (#2876)\n" +
59
60
  "\n" +
60
61
  "options:\n" +
61
62
  " --repo OWNER/REPO Repository (default: origin / DEFT_TRIAGE_REPO)\n" +
@@ -1,6 +1,6 @@
1
1
  import { MONITORING_TIER_1, MONITORING_TIER_2, MONITORING_TIER_3 } from "./constants.js";
2
- /** Canonical Approach-1 platform primitives for review-monitor register/verify (#2655 / #2876). */
3
- export type PlatformPrimitive = "start_agent" | "spawn_subagent" | "cursor-task" | "sessions_spawn" | "openclaw-sessions-spawn";
2
+ /** Canonical Approach-1 platform primitives for review-monitor register/verify (#2655 / #2876 / #3134). */
3
+ export type PlatformPrimitive = "start_agent" | "spawn_subagent" | "cursor-task" | "claude-agent" | "sessions_spawn" | "openclaw-sessions-spawn";
4
4
  /** Accepted `--platform-primitive` values (register CLI + help text). */
5
5
  export declare const PLATFORM_PRIMITIVES: readonly PlatformPrimitive[];
6
6
  export declare const PLATFORM_PRIMITIVE_SET: Set<string>;
@@ -11,8 +11,12 @@ export interface MonitoringTierProbe {
11
11
  }
12
12
  /**
13
13
  * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
14
- * (#1877 / #2655 / #2876). Prefer `task platform:capabilities` when available (#1357);
14
+ * (#1877 / #2655 / #2876 / #3134). Prefer `task platform:capabilities` when available (#1357);
15
15
  * this probe does not block MVP.
16
+ *
17
+ * Ordered env probe (must match skill matrix placement; Claude after Cursor so bare
18
+ * Task / CURSOR_* never misclassify Claude Code as cursor-composer):
19
+ * start_agent → WARP_* → Cursor → Claude Code → OpenClaw → grok-build → Tier2 → Tier3.
16
20
  */
17
21
  export declare function probeMonitoringTier(environ?: NodeJS.ProcessEnv): MonitoringTierProbe;
18
22
  export declare function isTier1(probe: MonitoringTierProbe): boolean;
@@ -5,6 +5,7 @@ export const PLATFORM_PRIMITIVES = [
5
5
  "start_agent",
6
6
  "spawn_subagent",
7
7
  "cursor-task",
8
+ "claude-agent",
8
9
  "sessions_spawn",
9
10
  "openclaw-sessions-spawn",
10
11
  ];
@@ -28,8 +29,12 @@ function probeOverride(environ) {
28
29
  }
29
30
  /**
30
31
  * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
31
- * (#1877 / #2655 / #2876). Prefer `task platform:capabilities` when available (#1357);
32
+ * (#1877 / #2655 / #2876 / #3134). Prefer `task platform:capabilities` when available (#1357);
32
33
  * this probe does not block MVP.
34
+ *
35
+ * Ordered env probe (must match skill matrix placement; Claude after Cursor so bare
36
+ * Task / CURSOR_* never misclassify Claude Code as cursor-composer):
37
+ * start_agent → WARP_* → Cursor → Claude Code → OpenClaw → grok-build → Tier2 → Tier3.
33
38
  */
34
39
  export function probeMonitoringTier(environ = process.env) {
35
40
  const override = probeOverride(environ);
@@ -53,6 +58,18 @@ export function probeMonitoringTier(environ = process.env) {
53
58
  };
54
59
  }
55
60
  const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
61
+ // Claude Code: Claude-unique env signals only — never bare "Task" (#3134).
62
+ // CLAUDECODE is set in Claude Code tool/hook subprocesses (Anthropic docs).
63
+ // DEFT_PROBE_CLAUDE_CODE / DEFT_AGENT_RUNTIME=claude-code are explicit overrides.
64
+ // Cursor already short-circuited above, so CURSOR_* never falls into this branch.
65
+ if (envTruthy(environ, "DEFT_PROBE_CLAUDE_CODE") ||
66
+ envTruthy(environ, "DEFT_HAS_CLAUDE_AGENT") ||
67
+ envTruthy(environ, "CLAUDECODE") ||
68
+ envTruthy(environ, "CLAUDE_CODE") ||
69
+ runtime === "claude-code" ||
70
+ runtime === "claude") {
71
+ return { tier: MONITORING_TIER_1, primitive: "claude-agent", descriptor: "claude-code" };
72
+ }
56
73
  // OpenClaw: sessions_spawn is the Tier-1 Approach 1 primitive (#2876).
57
74
  // Alias openclaw-sessions-spawn accepted on register for explicit naming.
58
75
  if (envTruthy(environ, "DEFT_PROBE_SESSIONS_SPAWN") ||
@@ -7,6 +7,24 @@ import { defaultSubagentStatusDir, fetchActiveMonitorFromGithub, readReviewMonit
7
7
  import { isTier1, probeMonitoringTier } from "./tier-detection.js";
8
8
  function spawnRedirect(probe) {
9
9
  const primitive = probe.primitive ?? "sub-agent";
10
+ // Claude Code / Cursor nested-leaf boundary (#2797 / #3134): lead with leaf-safe
11
+ // ownership so implementation leaves never treat nested Task/Agent spawn as the
12
+ // default instruction. Top-level parents that own the primitive still get the
13
+ // Approach 1 background path second.
14
+ if (primitive === "claude-agent" || primitive === "cursor-task") {
15
+ return (`Ownership path for ${primitive} (#2797 / #3134):\n` +
16
+ " 1. Implementation leaf (drive-to: merge-ready): keep ownership in THIS process " +
17
+ "via blocking dual-invoke `pr:watch` (`deft pr:watch <N>` then `task deft:pr:watch -- <N>`). " +
18
+ `Do NOT nested-spawn another ${primitive} review-monitor.\n` +
19
+ " 2. Or scope leaf `stop-at: pr-open` so the parent/orchestrator that owns " +
20
+ `${primitive} spawns a sibling monitor and registers it.\n` +
21
+ " 3. Top-level parent/orchestrator only: spawn Approach 1 via " +
22
+ `${primitive} (background), include templates/agent-prompt-preamble.md and ` +
23
+ "templates/swarm-greptile-poller-prompt.md, then:\n" +
24
+ " task review-monitor:register -- --pr <N> --monitor-agent-id <id> " +
25
+ `--platform-primitive ${primitive}\n` +
26
+ "Re-run: task verify:review-monitor -- --pr <N>");
27
+ }
10
28
  return (`Spawn an Approach 1 review-monitor via ${primitive} (background), include ` +
11
29
  "`templates/agent-prompt-preamble.md` and `templates/swarm-greptile-poller-prompt.md`, " +
12
30
  "then register:\n" +
@@ -7,6 +7,8 @@ export * from "./demote.js";
7
7
  export * from "./main.js";
8
8
  export * from "./open-umbrella-warning.js";
9
9
  export * from "./project-context.js";
10
+ export * from "./promote-from-issue.js";
11
+ export * from "./promote-path.js";
10
12
  export * from "./transition.js";
11
13
  export * from "./undo.js";
12
14
  export * from "./vbrief-json.js";
@@ -7,6 +7,8 @@ export * from "./demote.js";
7
7
  export * from "./main.js";
8
8
  export * from "./open-umbrella-warning.js";
9
9
  export * from "./project-context.js";
10
+ export * from "./promote-from-issue.js";
11
+ export * from "./promote-path.js";
10
12
  export * from "./transition.js";
11
13
  export * from "./undo.js";
12
14
  export * from "./vbrief-json.js";
@@ -6,6 +6,16 @@ export interface LifecycleArgs {
6
6
  force?: boolean;
7
7
  batch?: boolean;
8
8
  batchFiles?: string[];
9
+ /** Promote from triage-cache issue number (#1136). */
10
+ fromIssue?: number;
11
+ /** Repo slug for --from-issue (#1136). */
12
+ repo?: string;
13
+ /** Missing decision hard-fails (#1136). */
14
+ strict?: boolean;
15
+ /** Skip triage reciprocity gate (#1136). */
16
+ forceNoCache?: boolean;
17
+ /** Disambiguate multiple proposed artifacts for --from-issue (#1136). */
18
+ pathFlag?: string;
9
19
  /** Explicit non-delivery disposition for code-bearing complete (#3041). */
10
20
  nonDeliveryDisposition?: NonDeliveryDisposition;
11
21
  /** Optional delivery evidence flags for complete (#3041). */