@tea-agent/loop-agent 0.4.0 → 0.5.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 (106) hide show
  1. package/AGENTS.md +142 -142
  2. package/CHANGELOG.md +98 -106
  3. package/README.md +195 -195
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/validate-dag.js +14 -1
  7. package/dist/commands/init.js +677 -489
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/config-core.js +3 -2
  11. package/dist/executors/cursor-executor.js +1 -1
  12. package/dist/executors/model-routing.js +43 -0
  13. package/dist/governance/manifest-types.js +9 -1
  14. package/dist/task/runtime.js +27 -27
  15. package/dist/worker/pool/run-store.js +9 -1
  16. package/dist/workflows/dag/canvas-observer.js +275 -275
  17. package/dist/workflows/dag/init-hybrid.js +4 -13
  18. package/dist/workflows/dag/skill-instructions.js +4 -0
  19. package/dist/workflows/dag/types.js +1 -1
  20. package/dist/workflows/dag/validate.js +3 -2
  21. package/docs/README.md +72 -72
  22. package/docs/agent-dag-recovery-playbook.md +184 -184
  23. package/docs/agent-dag-runner.md +42 -42
  24. package/docs/architecture/runtime-boundaries.md +147 -147
  25. package/docs/cursor-executor-usage.md +25 -25
  26. package/docs/decisions/README.md +3 -3
  27. package/docs/design/README.md +36 -36
  28. package/docs/development-principles.md +73 -73
  29. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  30. package/docs/exec-plans/README.md +6 -6
  31. package/docs/exec-plans/active/README.md +7 -7
  32. package/docs/exec-plans/completed/README.md +19 -19
  33. package/docs/feature-workflow.md +186 -186
  34. package/docs/harness-methodology-debugging.md +153 -153
  35. package/docs/harness-methodology-tdd.md +130 -130
  36. package/docs/harness-methodology-verification.md +27 -27
  37. package/docs/init-surface.manifest.json +199 -175
  38. package/docs/loop-agent-harness.md +42 -42
  39. package/docs/production-readiness.md +96 -96
  40. package/docs/progress/README.md +3 -3
  41. package/docs/reports/README.md +5 -5
  42. package/docs/skills/README.md +6 -6
  43. package/docs/skills/vetted-skill-registry.md +26 -26
  44. package/docs/templates/adr.md +60 -60
  45. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  46. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  47. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  48. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  49. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  50. package/docs/templates/agent-dag-report.schema.json +454 -454
  51. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  52. package/docs/templates/agent-dag.base.json +195 -195
  53. package/docs/templates/agent-dag.final-verification.json +190 -190
  54. package/docs/templates/agent-dag.schema.json +316 -316
  55. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  56. package/docs/templates/exec-plan.md +64 -64
  57. package/docs/templates/feature-spec.md +53 -53
  58. package/docs/templates/hybrid-dag.json +193 -193
  59. package/docs/templates/init-evolution-review.md +33 -33
  60. package/docs/templates/production-readiness-checklist.md +57 -57
  61. package/docs/templates/progress-log.md +17 -17
  62. package/docs/templates/project-start-checklist.md +9 -9
  63. package/docs/templates/qa-report.md +48 -48
  64. package/docs/templates/sprint-contract.md +29 -29
  65. package/docs/verification-matrix.md +41 -41
  66. package/examples/decision-gate-agent-dag.json +123 -123
  67. package/examples/example-dag.json +51 -51
  68. package/examples/hybrid-loop-agent-dag.json +194 -194
  69. package/harness.json +69 -94
  70. package/package.json +66 -66
  71. package/skills/ai-engineering-context/SKILL.md +48 -48
  72. package/skills/code-review-core/SKILL.md +20 -20
  73. package/skills/codebase-scout/SKILL.md +19 -19
  74. package/skills/init-capability-evolution/SKILL.md +69 -69
  75. package/skills/loop-agent/SKILL.md +147 -147
  76. package/skills/loop-agent/references/README.md +67 -67
  77. package/skills/loop-agent/references/command-reference.md +403 -403
  78. package/skills/loop-agent/references/harness-policy.md +259 -259
  79. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  80. package/skills/loop-agent/references/learned/README.md +21 -21
  81. package/skills/loop-agent/references/long-running-loop.md +59 -59
  82. package/skills/loop-agent/references/model-routing.md +36 -36
  83. package/skills/loop-agent/references/multi-worktree.md +54 -54
  84. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  85. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  86. package/skills/loop-agent/references/pi-prompt.md +23 -23
  87. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  88. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  89. package/skills/loop-agent/references/task-workflow.md +84 -84
  90. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  91. package/skills/requesting-code-review/SKILL.md +101 -101
  92. package/skills/requesting-code-review/code-reviewer.md +168 -168
  93. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  94. package/skills/systematic-debugging/SKILL.md +296 -296
  95. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  96. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  97. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  98. package/skills/systematic-debugging/find-polluter.sh +63 -63
  99. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  100. package/skills/systematic-debugging/test-academic.md +14 -14
  101. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  102. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  103. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  104. package/skills/test-driven-development/SKILL.md +20 -20
  105. package/skills/verification-before-completion/SKILL.md +154 -154
  106. package/skills/webapp-testing/SKILL.md +19 -19
@@ -195,280 +195,280 @@ export function createDagCanvasObserver(opts) {
195
195
  flush: () => writer.flush(),
196
196
  };
197
197
  }
198
- const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
199
- import {
200
- Card,
201
- CardBody,
202
- CardHeader,
203
- Divider,
204
- Grid,
205
- H1,
206
- H2,
207
- Pill,
208
- Row,
209
- Stack,
210
- Stat,
211
- Text,
212
- computeDAGLayout,
213
- useHostTheme,
214
- } from 'cursor/canvas';
215
- import { useMemo } from 'react';
216
-
217
- type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
218
- type Complexity = 'HIGH' | 'MED' | 'LOW';
219
- type Executor = 'cursor' | 'pi' | 'shell' | 'static';
220
-
221
- interface TaskState {
222
- id: string;
223
- depends_on: string[];
224
- complexity: Complexity;
225
- executor: Executor;
226
- subtask_prompt: string;
227
- status: TaskStatus;
228
- model: string;
229
- startedAt?: number;
230
- finishedAt?: number;
231
- resultText?: string;
232
- errorMessage?: string;
233
- inputTokens?: number;
234
- outputTokens?: number;
235
- durationMs?: number;
236
- skippedReason?: string;
237
- }
238
-
239
- interface RunState {
240
- title: string;
241
- runId: string;
242
- startedAt: number;
243
- finishedAt?: number;
244
- runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
245
- runMessage?: string;
246
- tasks: TaskState[];
198
+ const CANVAS_HEADER = `/* AUTO-GENERATED by . run-dag canvas observer. Derived view only — canonical state lives in .harness/dag-runs/. */
199
+ import {
200
+ Card,
201
+ CardBody,
202
+ CardHeader,
203
+ Divider,
204
+ Grid,
205
+ H1,
206
+ H2,
207
+ Pill,
208
+ Row,
209
+ Stack,
210
+ Stat,
211
+ Text,
212
+ computeDAGLayout,
213
+ useHostTheme,
214
+ } from 'cursor/canvas';
215
+ import { useMemo } from 'react';
216
+
217
+ type TaskStatus = 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'SKIPPED';
218
+ type Complexity = 'HIGH' | 'MED' | 'LOW';
219
+ type Executor = 'cursor' | 'pi' | 'shell' | 'static';
220
+
221
+ interface TaskState {
222
+ id: string;
223
+ depends_on: string[];
224
+ complexity: Complexity;
225
+ executor: Executor;
226
+ subtask_prompt: string;
227
+ status: TaskStatus;
228
+ model: string;
229
+ startedAt?: number;
230
+ finishedAt?: number;
231
+ resultText?: string;
232
+ errorMessage?: string;
233
+ inputTokens?: number;
234
+ outputTokens?: number;
235
+ durationMs?: number;
236
+ skippedReason?: string;
237
+ }
238
+
239
+ interface RunState {
240
+ title: string;
241
+ runId: string;
242
+ startedAt: number;
243
+ finishedAt?: number;
244
+ runOutcome?: 'SUCCESS' | 'FAILED' | 'INTERRUPTED' | 'PAUSED';
245
+ runMessage?: string;
246
+ tasks: TaskState[];
247
247
  }`;
248
- const CANVAS_BODY = String.raw `const NODE_W = 200;
249
- const NODE_H = 72;
250
-
251
- function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
252
- switch (status) {
253
- case 'PENDING':
254
- return 'neutral';
255
- case 'RUNNING':
256
- return 'info';
257
- case 'FINISHED':
258
- return 'success';
259
- case 'ERROR':
260
- return 'warning';
261
- case 'SKIPPED':
262
- return 'neutral';
263
- }
264
- }
265
-
266
- function formatDuration(ms?: number): string {
267
- if (ms === undefined) return '—';
268
- if (ms < 1000) return ms + 'ms';
269
- const s = ms / 1000;
270
- if (s < 60) return s.toFixed(1) + 's';
271
- const m = Math.floor(s / 60);
272
- const rem = Math.round(s - m * 60);
273
- return m + 'm ' + rem + 's';
274
- }
275
-
276
- function elapsed(state: RunState): number {
277
- const end = state.finishedAt ?? Date.now();
278
- return end - state.startedAt;
279
- }
280
-
281
- function DAGGraph({ state }: { state: RunState }): JSX.Element {
282
- const theme = useHostTheme();
283
- const layout = computeDAGLayout({
284
- nodes: state.tasks.map((t) => ({ id: t.id })),
285
- edges: state.tasks.flatMap((t) =>
286
- t.depends_on.map((d) => ({ from: d, to: t.id })),
287
- ),
288
- direction: 'vertical',
289
- nodeWidth: NODE_W,
290
- nodeHeight: NODE_H,
291
- rankGap: 72,
292
- nodeGap: 40,
293
- padding: 24,
294
- });
295
- const byId = new Map(state.tasks.map((t) => [t.id, t]));
296
-
297
- return (
298
- <svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
299
- {layout.edges.map((e, i) => (
300
- <line
301
- key={'e' + i}
302
- x1={e.sourceX}
303
- y1={e.sourceY}
304
- x2={e.targetX}
305
- y2={e.targetY}
306
- stroke={theme.stroke.secondary}
307
- strokeWidth={1.25}
308
- markerEnd="url(#dag-arrow)"
309
- />
310
- ))}
311
- <defs>
312
- <marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
313
- <path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
314
- </marker>
315
- </defs>
316
- {layout.nodes.map((n) => {
317
- const t = byId.get(n.id);
318
- if (!t) return null;
319
- return (
320
- <g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
321
- <rect
322
- width={NODE_W}
323
- height={NODE_H}
324
- rx={8}
325
- ry={8}
326
- fill={theme.fill.secondary}
327
- stroke={theme.stroke.secondary}
328
- strokeWidth={t.status === 'RUNNING' ? 2 : 1}
329
- />
330
- <text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
331
- {t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
332
- </text>
333
- <text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
334
- {t.executor} · {t.complexity} · {t.status}
335
- </text>
336
- <text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
337
- {formatDuration(t.durationMs)}
338
- </text>
339
- </g>
340
- );
341
- })}
342
- </svg>
343
- );
344
- }
345
-
346
- export default function DagRun(): JSX.Element {
347
- const counts = useMemo(
348
- () =>
349
- STATE.tasks.reduce(
350
- (acc, t) => {
351
- acc.total += 1;
352
- const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
353
- if (key in acc) acc[key] += 1;
354
- return acc;
355
- },
356
- { total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
357
- ),
358
- [],
359
- );
360
- const isFinal = STATE.finishedAt !== undefined;
361
- const statusLabel =
362
- STATE.runOutcome === 'PAUSED'
363
- ? 'PAUSED'
364
- : STATE.runOutcome === 'FAILED'
365
- ? 'FAILED'
366
- : isFinal
367
- ? 'COMPLETE'
368
- : 'RUNNING';
369
- const statusTone =
370
- STATE.runOutcome === 'FAILED'
371
- ? 'danger'
372
- : STATE.runOutcome === 'PAUSED'
373
- ? 'warning'
374
- : isFinal
375
- ? 'success'
376
- : 'info';
377
-
378
- return (
379
- <Stack gap={20}>
380
- <Stack gap={6}>
381
- <H1>{STATE.title}</H1>
382
- <Text tone="secondary" size="small">
383
- run {STATE.runId}
384
- </Text>
385
- <Row gap={10} align="center">
386
- <Pill tone={statusTone} active size="sm">
387
- {statusLabel}
388
- </Pill>
389
- <Text tone="secondary" size="small">
390
- {counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
391
- </Text>
392
- </Row>
393
- {STATE.runMessage ? (
394
- <Text tone="secondary" size="small">
395
- {STATE.runMessage}
396
- </Text>
397
- ) : null}
398
- </Stack>
399
-
400
- <Grid columns={6} gap={12}>
401
- <Stat value={String(counts.total)} label="Total" />
402
- <Stat value={String(counts.pending)} label="Pending" />
403
- <Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
404
- <Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
405
- <Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
406
- <Stat value={String(counts.skipped)} label="Skipped" />
407
- </Grid>
408
-
409
- <Divider />
410
-
411
- <Stack gap={12}>
412
- <H2>Graph</H2>
413
- <DAGGraph state={STATE} />
414
- </Stack>
415
-
416
- <Divider />
417
-
418
- <Stack gap={12}>
419
- <H2>Tasks</H2>
420
- <Stack gap={10}>
421
- {STATE.tasks.map((t) => (
422
- <Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
423
- <CardHeader
424
- trailing={
425
- <Row gap={6} align="center">
426
- <Pill tone="neutral" size="sm">
427
- {t.executor}
428
- </Pill>
429
- <Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
430
- {t.status}
431
- </Pill>
432
- </Row>
433
- }
434
- >
435
- {t.id}
436
- </CardHeader>
437
- <CardBody>
438
- <Stack gap={8}>
439
- <Text tone="secondary" size="small">
440
- Model {t.model}
441
- {t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
442
- {t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
443
- </Text>
444
- <Text size="small">
445
- <Text as="span" weight="semibold">
446
- Prompt:{' '}
447
- </Text>
448
- {t.subtask_prompt}
449
- </Text>
450
- {t.resultText ? (
451
- <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
452
- {t.resultText}
453
- {t.status === 'RUNNING' ? '\u2588' : ''}
454
- </pre>
455
- ) : null}
456
- {t.skippedReason ? (
457
- <Text size="small" tone="secondary">
458
- Skipped: {t.skippedReason}
459
- </Text>
460
- ) : null}
461
- {t.errorMessage ? (
462
- <Text size="small" tone="secondary">
463
- {t.errorMessage}
464
- </Text>
465
- ) : null}
466
- </Stack>
467
- </CardBody>
468
- </Card>
469
- ))}
470
- </Stack>
471
- </Stack>
472
- </Stack>
473
- );
248
+ const CANVAS_BODY = String.raw `const NODE_W = 200;
249
+ const NODE_H = 72;
250
+
251
+ function pillToneFor(status: TaskStatus): 'neutral' | 'info' | 'success' | 'warning' {
252
+ switch (status) {
253
+ case 'PENDING':
254
+ return 'neutral';
255
+ case 'RUNNING':
256
+ return 'info';
257
+ case 'FINISHED':
258
+ return 'success';
259
+ case 'ERROR':
260
+ return 'warning';
261
+ case 'SKIPPED':
262
+ return 'neutral';
263
+ }
264
+ }
265
+
266
+ function formatDuration(ms?: number): string {
267
+ if (ms === undefined) return '—';
268
+ if (ms < 1000) return ms + 'ms';
269
+ const s = ms / 1000;
270
+ if (s < 60) return s.toFixed(1) + 's';
271
+ const m = Math.floor(s / 60);
272
+ const rem = Math.round(s - m * 60);
273
+ return m + 'm ' + rem + 's';
274
+ }
275
+
276
+ function elapsed(state: RunState): number {
277
+ const end = state.finishedAt ?? Date.now();
278
+ return end - state.startedAt;
279
+ }
280
+
281
+ function DAGGraph({ state }: { state: RunState }): JSX.Element {
282
+ const theme = useHostTheme();
283
+ const layout = computeDAGLayout({
284
+ nodes: state.tasks.map((t) => ({ id: t.id })),
285
+ edges: state.tasks.flatMap((t) =>
286
+ t.depends_on.map((d) => ({ from: d, to: t.id })),
287
+ ),
288
+ direction: 'vertical',
289
+ nodeWidth: NODE_W,
290
+ nodeHeight: NODE_H,
291
+ rankGap: 72,
292
+ nodeGap: 40,
293
+ padding: 24,
294
+ });
295
+ const byId = new Map(state.tasks.map((t) => [t.id, t]));
296
+
297
+ return (
298
+ <svg width={layout.width} height={layout.height} style={{ display: 'block', maxWidth: '100%' }}>
299
+ {layout.edges.map((e, i) => (
300
+ <line
301
+ key={'e' + i}
302
+ x1={e.sourceX}
303
+ y1={e.sourceY}
304
+ x2={e.targetX}
305
+ y2={e.targetY}
306
+ stroke={theme.stroke.secondary}
307
+ strokeWidth={1.25}
308
+ markerEnd="url(#dag-arrow)"
309
+ />
310
+ ))}
311
+ <defs>
312
+ <marker id="dag-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
313
+ <path d="M 0 0 L 10 5 L 0 10 z" fill={theme.stroke.secondary} />
314
+ </marker>
315
+ </defs>
316
+ {layout.nodes.map((n) => {
317
+ const t = byId.get(n.id);
318
+ if (!t) return null;
319
+ return (
320
+ <g key={n.id} transform={'translate(' + n.x + ',' + n.y + ')'}>
321
+ <rect
322
+ width={NODE_W}
323
+ height={NODE_H}
324
+ rx={8}
325
+ ry={8}
326
+ fill={theme.fill.secondary}
327
+ stroke={theme.stroke.secondary}
328
+ strokeWidth={t.status === 'RUNNING' ? 2 : 1}
329
+ />
330
+ <text x={12} y={22} fontSize={13} fontWeight={600} fill={theme.text.primary}>
331
+ {t.id.length > 22 ? t.id.slice(0, 21) + '…' : t.id}
332
+ </text>
333
+ <text x={12} y={42} fontSize={10.5} fill={theme.text.secondary}>
334
+ {t.executor} · {t.complexity} · {t.status}
335
+ </text>
336
+ <text x={12} y={58} fontSize={10.5} fill={theme.text.tertiary}>
337
+ {formatDuration(t.durationMs)}
338
+ </text>
339
+ </g>
340
+ );
341
+ })}
342
+ </svg>
343
+ );
344
+ }
345
+
346
+ export default function DagRun(): JSX.Element {
347
+ const counts = useMemo(
348
+ () =>
349
+ STATE.tasks.reduce(
350
+ (acc, t) => {
351
+ acc.total += 1;
352
+ const key = t.status.toLowerCase() as 'pending' | 'running' | 'finished' | 'error' | 'skipped';
353
+ if (key in acc) acc[key] += 1;
354
+ return acc;
355
+ },
356
+ { total: 0, pending: 0, running: 0, finished: 0, error: 0, skipped: 0 },
357
+ ),
358
+ [],
359
+ );
360
+ const isFinal = STATE.finishedAt !== undefined;
361
+ const statusLabel =
362
+ STATE.runOutcome === 'PAUSED'
363
+ ? 'PAUSED'
364
+ : STATE.runOutcome === 'FAILED'
365
+ ? 'FAILED'
366
+ : isFinal
367
+ ? 'COMPLETE'
368
+ : 'RUNNING';
369
+ const statusTone =
370
+ STATE.runOutcome === 'FAILED'
371
+ ? 'danger'
372
+ : STATE.runOutcome === 'PAUSED'
373
+ ? 'warning'
374
+ : isFinal
375
+ ? 'success'
376
+ : 'info';
377
+
378
+ return (
379
+ <Stack gap={20}>
380
+ <Stack gap={6}>
381
+ <H1>{STATE.title}</H1>
382
+ <Text tone="secondary" size="small">
383
+ run {STATE.runId}
384
+ </Text>
385
+ <Row gap={10} align="center">
386
+ <Pill tone={statusTone} active size="sm">
387
+ {statusLabel}
388
+ </Pill>
389
+ <Text tone="secondary" size="small">
390
+ {counts.total} tasks · elapsed {formatDuration(elapsed(STATE))}
391
+ </Text>
392
+ </Row>
393
+ {STATE.runMessage ? (
394
+ <Text tone="secondary" size="small">
395
+ {STATE.runMessage}
396
+ </Text>
397
+ ) : null}
398
+ </Stack>
399
+
400
+ <Grid columns={6} gap={12}>
401
+ <Stat value={String(counts.total)} label="Total" />
402
+ <Stat value={String(counts.pending)} label="Pending" />
403
+ <Stat value={String(counts.running)} label="Running" tone={counts.running > 0 ? 'info' : undefined} />
404
+ <Stat value={String(counts.finished)} label="Finished" tone={counts.finished > 0 ? 'success' : undefined} />
405
+ <Stat value={String(counts.error)} label="Errored" tone={counts.error > 0 ? 'danger' : undefined} />
406
+ <Stat value={String(counts.skipped)} label="Skipped" />
407
+ </Grid>
408
+
409
+ <Divider />
410
+
411
+ <Stack gap={12}>
412
+ <H2>Graph</H2>
413
+ <DAGGraph state={STATE} />
414
+ </Stack>
415
+
416
+ <Divider />
417
+
418
+ <Stack gap={12}>
419
+ <H2>Tasks</H2>
420
+ <Stack gap={10}>
421
+ {STATE.tasks.map((t) => (
422
+ <Card key={t.id} collapsible defaultOpen={t.status !== 'PENDING'}>
423
+ <CardHeader
424
+ trailing={
425
+ <Row gap={6} align="center">
426
+ <Pill tone="neutral" size="sm">
427
+ {t.executor}
428
+ </Pill>
429
+ <Pill tone={pillToneFor(t.status)} active={t.status !== 'PENDING'} size="sm">
430
+ {t.status}
431
+ </Pill>
432
+ </Row>
433
+ }
434
+ >
435
+ {t.id}
436
+ </CardHeader>
437
+ <CardBody>
438
+ <Stack gap={8}>
439
+ <Text tone="secondary" size="small">
440
+ Model {t.model}
441
+ {t.depends_on.length > 0 ? ' · depends on ' + t.depends_on.join(', ') : ''}
442
+ {t.durationMs !== undefined ? ' · ' + formatDuration(t.durationMs) : ''}
443
+ </Text>
444
+ <Text size="small">
445
+ <Text as="span" weight="semibold">
446
+ Prompt:{' '}
447
+ </Text>
448
+ {t.subtask_prompt}
449
+ </Text>
450
+ {t.resultText ? (
451
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
452
+ {t.resultText}
453
+ {t.status === 'RUNNING' ? '\u2588' : ''}
454
+ </pre>
455
+ ) : null}
456
+ {t.skippedReason ? (
457
+ <Text size="small" tone="secondary">
458
+ Skipped: {t.skippedReason}
459
+ </Text>
460
+ ) : null}
461
+ {t.errorMessage ? (
462
+ <Text size="small" tone="secondary">
463
+ {t.errorMessage}
464
+ </Text>
465
+ ) : null}
466
+ </Stack>
467
+ </CardBody>
468
+ </Card>
469
+ ))}
470
+ </Stack>
471
+ </Stack>
472
+ </Stack>
473
+ );
474
474
  }`;
@@ -2,7 +2,7 @@ import { access, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { assertValidDagSpec } from "./validate.js";
5
- import { parseDagSpec, } from "./types.js";
5
+ import { DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
6
6
  import { pathMatchesPattern } from "../../shared/git-progress.js";
7
7
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
8
8
  import { resolveAdapter } from "../../adapters/index.js";
@@ -10,6 +10,7 @@ import { loadHarnessManifest } from "../../governance/harness.js";
10
10
  import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
11
11
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
12
12
  import { resolveVerifyPreset } from "../../executors/shell-verification.js";
13
+ import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
13
14
  const REQUIREMENT_FILE = "需求.md";
14
15
  const CONSTRAINT_FILE = "执行约束.md";
15
16
  const MAX_SOURCE_EXCERPT_CHARS = 2000;
@@ -301,6 +302,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
301
302
  constraintMarkdown,
302
303
  taskConfig,
303
304
  enabledExecutors: resolveEnabledExecutors(manifest.executors),
305
+ executorModelMatrix: resolveExecutorModelMatrices(manifest),
304
306
  verifyCommands,
305
307
  };
306
308
  }
@@ -336,18 +338,7 @@ export function buildStandardHybridDagFromTask(sources) {
336
338
  contextProfile: taskConfig.contextProfile,
337
339
  },
338
340
  skillsByRole: HYBRID_SKILLS_BY_ROLE,
339
- executorModels: {
340
- cursor: {
341
- LOW: "composer-2.5",
342
- MED: "composer-2.5",
343
- HIGH: "composer-2.5",
344
- },
345
- pi: {
346
- LOW: "gpt-5.3-codex-spark",
347
- MED: "glm-5.2",
348
- HIGH: "gpt-5.5",
349
- },
350
- },
341
+ executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
351
342
  tasks: [
352
343
  {
353
344
  id: "contract-pi",
@@ -42,6 +42,10 @@ function buildSkillCandidatePaths(input) {
42
42
  const piSkillsDir = path.join(input.homeDir, ".pi", "agent", "skills");
43
43
  candidates.push(normalizeCandidate(path.join(piSkillsDir, input.name, "SKILL.md")), normalizeCandidate(path.join(piSkillsDir, `${input.name}.md`)));
44
44
  candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
45
+ // Agent-compatible project path (e.g. OpenCode). `skills/` stays the primary
46
+ // loop-agent repo-local path; `.agents/skills` is searched after it and before
47
+ // bundled skills so a project with only the mirror can still resolve.
48
+ candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
45
49
  if (input.name === "loop-agent") {
46
50
  candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
47
51
  }
@@ -251,7 +251,7 @@ export const DEFAULT_DAG_EXECUTOR_MODELS = {
251
251
  cursor: {
252
252
  LOW: "composer-2.5",
253
253
  MED: "composer-2.5",
254
- HIGH: "composer-2.5",
254
+ HIGH: "gpt-5.5",
255
255
  },
256
256
  pi: {
257
257
  LOW: "gpt-5.3-codex-spark",
@@ -428,7 +428,7 @@ function validateDecisionGateTaskConfig(task, issues) {
428
428
  });
429
429
  }
430
430
  }
431
- export function collectExecutorModelWarnings(spec) {
431
+ export function collectExecutorModelWarnings(spec, expectedMatrices = DEFAULT_DAG_EXECUTOR_MODELS) {
432
432
  const warnings = [];
433
433
  const executors = Object.keys(DEFAULT_DAG_EXECUTOR_MODELS);
434
434
  for (const executor of executors) {
@@ -436,11 +436,12 @@ export function collectExecutorModelWarnings(spec) {
436
436
  if (!actualByComplexity) {
437
437
  continue;
438
438
  }
439
+ const expectedByComplexity = expectedMatrices[executor] ?? DEFAULT_DAG_EXECUTOR_MODELS[executor];
439
440
  for (const [complexityKey, actual] of Object.entries(actualByComplexity)) {
440
441
  if (actual === undefined) {
441
442
  continue;
442
443
  }
443
- const expected = DEFAULT_DAG_EXECUTOR_MODELS[executor][complexityKey];
444
+ const expected = expectedByComplexity[complexityKey];
444
445
  if (actual !== expected) {
445
446
  warnings.push({
446
447
  executor,