@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.
- package/AGENTS.md +142 -142
- package/CHANGELOG.md +98 -106
- package/README.md +195 -195
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/validate-dag.js +14 -1
- package/dist/commands/init.js +677 -489
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/config-core.js +3 -2
- package/dist/executors/cursor-executor.js +1 -1
- package/dist/executors/model-routing.js +43 -0
- package/dist/governance/manifest-types.js +9 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/pool/run-store.js +9 -1
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/init-hybrid.js +4 -13
- package/dist/workflows/dag/skill-instructions.js +4 -0
- package/dist/workflows/dag/types.js +1 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/README.md +72 -72
- package/docs/agent-dag-recovery-playbook.md +184 -184
- package/docs/agent-dag-runner.md +42 -42
- package/docs/architecture/runtime-boundaries.md +147 -147
- package/docs/cursor-executor-usage.md +25 -25
- package/docs/decisions/README.md +3 -3
- package/docs/design/README.md +36 -36
- package/docs/development-principles.md +73 -73
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +7 -7
- package/docs/exec-plans/completed/README.md +19 -19
- package/docs/feature-workflow.md +186 -186
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +199 -175
- package/docs/loop-agent-harness.md +42 -42
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +3 -3
- package/docs/reports/README.md +5 -5
- package/docs/skills/README.md +6 -6
- package/docs/skills/vetted-skill-registry.md +26 -26
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +454 -454
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/agent-dag.base.json +195 -195
- package/docs/templates/agent-dag.final-verification.json +190 -190
- package/docs/templates/agent-dag.schema.json +316 -316
- package/docs/templates/agent-dag.supervised-implementation.json +500 -500
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/hybrid-dag.json +193 -193
- package/docs/templates/init-evolution-review.md +33 -33
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/verification-matrix.md +41 -41
- package/examples/decision-gate-agent-dag.json +123 -123
- package/examples/example-dag.json +51 -51
- package/examples/hybrid-loop-agent-dag.json +194 -194
- package/harness.json +69 -94
- package/package.json +66 -66
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/init-capability-evolution/SKILL.md +69 -69
- package/skills/loop-agent/SKILL.md +147 -147
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +403 -403
- package/skills/loop-agent/references/harness-policy.md +259 -259
- package/skills/loop-agent/references/hybrid-dag.md +216 -216
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +59 -59
- package/skills/loop-agent/references/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
- package/skills/loop-agent/references/task-workflow.md +84 -84
- package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/SKILL.md +296 -296
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/test-driven-development/SKILL.md +20 -20
- package/skills/verification-before-completion/SKILL.md +154 -154
- 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
|
}
|
|
@@ -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 =
|
|
444
|
+
const expected = expectedByComplexity[complexityKey];
|
|
444
445
|
if (actual !== expected) {
|
|
445
446
|
warnings.push({
|
|
446
447
|
executor,
|