@tea-agent/loop-agent 0.10.0 → 0.12.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 +10 -2
- package/CHANGELOG.md +91 -24
- package/README.md +84 -12
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +38 -2
- package/dist/application/dag/run-dag.js +11 -27
- package/dist/application/dag/validate-dag.js +13 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +547 -95
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +488 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +29 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +32 -3
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +338 -83
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +165 -21
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/static/api.js +27 -0
- package/dist/worker/observe/static/app.js +120 -2317
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +128 -0
- package/dist/worker/observe/static/router.js +85 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1720 -495
- package/dist/worker/observe/static/views/batch.js +226 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +477 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +442 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +453 -0
- package/dist/worker/observe/static/views/pool.js +347 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +260 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +797 -27
- package/dist/workflows/dag/node-execution.js +183 -35
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +77 -17
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +529 -0
- package/dist/workflows/dag/types.js +86 -10
- package/dist/workflows/dag/validate.js +73 -12
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +28 -3
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +140 -0
- package/docs/architecture/evolution.md +53 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +43 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +1 -3
- package/docs/exec-plans/completed/README.md +23 -0
- package/docs/feature-workflow.md +78 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +50 -1
- package/docs/reports/README.md +90 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +23 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +70 -3
- package/docs/templates/agent-dag.supervised-implementation.json +9 -8
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +276 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +14 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +36 -20
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -5,10 +5,14 @@ import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/r
|
|
|
5
5
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
6
6
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
7
7
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
8
|
-
import {
|
|
8
|
+
import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
|
|
9
9
|
import { assertValidDagSpec } from "./validate.js";
|
|
10
|
+
import { assertRuntimeContractCompatible, DAG_CONTROLLER_CAPABILITIES, } from "./runtime-contract.js";
|
|
11
|
+
import { captureControllerIdentity, verifyControllerIdentityForResume, } from "./controller-identity.js";
|
|
12
|
+
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
10
13
|
import { normalizeDagPromptSources } from "./prompt-source.js";
|
|
11
14
|
import { relocateConvergenceArtifactPaths, relocateRunArtifactPaths, } from "./upstream-artifacts.js";
|
|
15
|
+
import { createSkillSnapshot, prepareSkillSnapshotForContinuation, writeSkillSnapshot, } from "./skill-snapshot.js";
|
|
12
16
|
import { buildNodePrompt, buildNodePromptWithResolvedSkillInstructions, executeDagNode, } from "./node-execution.js";
|
|
13
17
|
import { runConvergencePassController, shouldEnableDagConvergence, } from "./convergence/controller.js";
|
|
14
18
|
import { executeDagRanksOnce, isConditionSkippedReason, } from "./scheduler.js";
|
|
@@ -144,6 +148,15 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
144
148
|
}
|
|
145
149
|
export async function runDag(spec, opts) {
|
|
146
150
|
assertValidDagSpec(spec);
|
|
151
|
+
// Capability preflight must fail closed before any run directory or node work.
|
|
152
|
+
const runningIdentity = resolveRunningControllerIdentity();
|
|
153
|
+
if (!runningIdentity) {
|
|
154
|
+
throw new Error("running controller identity could not be resolved; refuse to create an unpinned DAG run");
|
|
155
|
+
}
|
|
156
|
+
assertRuntimeContractCompatible(spec, {
|
|
157
|
+
...DAG_CONTROLLER_CAPABILITIES,
|
|
158
|
+
controllerVersion: runningIdentity.packageVersion,
|
|
159
|
+
});
|
|
147
160
|
const { ranks } = topoSortToRanks(spec);
|
|
148
161
|
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
149
162
|
let runId = opts.runId;
|
|
@@ -162,6 +175,25 @@ export async function runDag(spec, opts) {
|
|
|
162
175
|
const runDir = activeRunDir;
|
|
163
176
|
await prepareActiveRunDir(runDir);
|
|
164
177
|
await writeRunSpec(runDir, spec);
|
|
178
|
+
// Freeze the running controller identity as a run-owned fact before the
|
|
179
|
+
// first canonical state write, reusing the identity resolved for preflight.
|
|
180
|
+
state.controllerIdentityRef = await captureControllerIdentity({
|
|
181
|
+
runDir,
|
|
182
|
+
identity: runningIdentity,
|
|
183
|
+
});
|
|
184
|
+
// Resolve and snapshot all skill profiles before any node executes.
|
|
185
|
+
try {
|
|
186
|
+
const snapshot = await createSkillSnapshot({
|
|
187
|
+
runId: state.runId,
|
|
188
|
+
spec,
|
|
189
|
+
cwd: opts.cwd,
|
|
190
|
+
mode: "run-start",
|
|
191
|
+
});
|
|
192
|
+
state.skillSnapshotRef = await writeSkillSnapshot(runDir, snapshot);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
throw new Error(`failed to create run-start skill snapshot: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
}
|
|
165
197
|
await writeRunState(runDir, state);
|
|
166
198
|
await notifyRunObserver(opts.observer, "onRunStart", state);
|
|
167
199
|
if (opts.dryRun || opts.initOnly) {
|
|
@@ -190,7 +222,6 @@ export async function runDag(spec, opts) {
|
|
|
190
222
|
});
|
|
191
223
|
}
|
|
192
224
|
finally {
|
|
193
|
-
await stopCursorWorkerIfRunning();
|
|
194
225
|
}
|
|
195
226
|
}
|
|
196
227
|
export async function resumeDagRun(opts) {
|
|
@@ -211,6 +242,20 @@ export async function resumeDagRun(opts) {
|
|
|
211
242
|
if (isTerminalDagRunStatus(state.status)) {
|
|
212
243
|
throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
|
|
213
244
|
}
|
|
245
|
+
// Runtime contract + controller identity must be re-verified before executing
|
|
246
|
+
// any remaining node on resume; drift fails closed.
|
|
247
|
+
const runningIdentity = resolveRunningControllerIdentity();
|
|
248
|
+
if (!runningIdentity) {
|
|
249
|
+
throw new Error("running controller identity could not be resolved; refuse to resume an unpinned DAG run");
|
|
250
|
+
}
|
|
251
|
+
assertRuntimeContractCompatible(spec, {
|
|
252
|
+
...DAG_CONTROLLER_CAPABILITIES,
|
|
253
|
+
controllerVersion: runningIdentity.packageVersion,
|
|
254
|
+
});
|
|
255
|
+
await verifyControllerIdentityForResume({
|
|
256
|
+
runDir,
|
|
257
|
+
state,
|
|
258
|
+
});
|
|
214
259
|
const decisionNodeId = state.humanDecisionNodeId;
|
|
215
260
|
if (!decisionNodeId) {
|
|
216
261
|
throw new Error(`dag run ${opts.runId} has no human decision checkpoint; was it approved?`);
|
|
@@ -221,6 +266,25 @@ export async function resumeDagRun(opts) {
|
|
|
221
266
|
catch {
|
|
222
267
|
throw new Error(`dag run ${opts.runId} is missing human-approval.json for node ${decisionNodeId}`);
|
|
223
268
|
}
|
|
269
|
+
// Validate or backfill before mutating runner/status for this resume attempt.
|
|
270
|
+
try {
|
|
271
|
+
const prepared = await prepareSkillSnapshotForContinuation({
|
|
272
|
+
runDir,
|
|
273
|
+
runId: state.runId,
|
|
274
|
+
spec,
|
|
275
|
+
state,
|
|
276
|
+
cwd: state.cwd,
|
|
277
|
+
ref: state.skillSnapshotRef,
|
|
278
|
+
});
|
|
279
|
+
if (prepared.backfilled) {
|
|
280
|
+
console.warn(`[dag resume] warning: run ${state.runId} has no skill snapshot ref; creating a legacy-resume-backfill that freezes remaining nodes only`);
|
|
281
|
+
state.skillSnapshotRef = prepared.ref;
|
|
282
|
+
await writeRunState(runDir, state);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
}
|
|
224
288
|
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
225
289
|
state.status = "running";
|
|
226
290
|
const resumedAt = new Date().toISOString();
|
|
@@ -249,7 +313,6 @@ export async function resumeDagRun(opts) {
|
|
|
249
313
|
});
|
|
250
314
|
}
|
|
251
315
|
finally {
|
|
252
|
-
await stopCursorWorkerIfRunning();
|
|
253
316
|
}
|
|
254
317
|
}
|
|
255
318
|
async function executeDagCheckpoint(input) {
|
|
@@ -314,10 +377,10 @@ async function executeDagCheckpoint(input) {
|
|
|
314
377
|
tasksById,
|
|
315
378
|
maxConcurrent,
|
|
316
379
|
persistState,
|
|
317
|
-
createExecuteNodeForRank: (
|
|
380
|
+
createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
|
|
318
381
|
baseExecuteNode,
|
|
319
382
|
customExecuteNode: input.executeNode,
|
|
320
|
-
|
|
383
|
+
rankWriterNodeIds,
|
|
321
384
|
tasksById,
|
|
322
385
|
meta: { runDir, runId: state.runId, spec },
|
|
323
386
|
}),
|
|
@@ -413,16 +476,10 @@ function finalizeTerminalRunStatus(state, taskCount) {
|
|
|
413
476
|
state.status = "failed";
|
|
414
477
|
}
|
|
415
478
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (isCursorWorkerRunning()) {
|
|
419
|
-
await stopCursorWorker();
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
function concurrentSiblingWriteSetsForNode(rankCursorNodeIds, nodeId, tasksById) {
|
|
423
|
-
if (rankCursorNodeIds.length <= 1)
|
|
479
|
+
function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
|
|
480
|
+
if (rankWriterNodeIds.length <= 1)
|
|
424
481
|
return [];
|
|
425
|
-
return
|
|
482
|
+
return rankWriterNodeIds
|
|
426
483
|
.filter((id) => id !== nodeId)
|
|
427
484
|
.map((id) => tasksById.get(id)?.writeSet ?? [])
|
|
428
485
|
.filter((writeSet) => writeSet.length > 0);
|
|
@@ -432,13 +489,16 @@ function buildRankAwareExecuteNode(input) {
|
|
|
432
489
|
return input.baseExecuteNode;
|
|
433
490
|
}
|
|
434
491
|
return async (nodeInput) => {
|
|
435
|
-
|
|
492
|
+
const isPiWriter = nodeInput.task.executor === "pi" &&
|
|
493
|
+
nodeInput.task.toolProfile === "write" &&
|
|
494
|
+
Boolean(nodeInput.model);
|
|
495
|
+
if (!isPiWriter) {
|
|
436
496
|
return input.baseExecuteNode(nodeInput);
|
|
437
497
|
}
|
|
438
498
|
const siblingWriteSets = nodeInput.task.writePolicy === "exclusive"
|
|
439
|
-
? concurrentSiblingWriteSetsForNode(input.
|
|
499
|
+
? concurrentSiblingWriteSetsForNode(input.rankWriterNodeIds, nodeInput.task.id, input.tasksById)
|
|
440
500
|
: [];
|
|
441
|
-
return
|
|
501
|
+
return executeDagPiNode({ ...nodeInput, model: nodeInput.model }, {
|
|
442
502
|
...input.meta,
|
|
443
503
|
concurrentSiblingWriteSets: siblingWriteSets.length > 0 ? siblingWriteSets : undefined,
|
|
444
504
|
writeGuardAttribution: siblingWriteSets.length > 0 ? "best-effort" : "per-node",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, } from "./types.js";
|
|
2
|
+
export const DAG_CONTROLLER_CAPABILITIES = {
|
|
3
|
+
schemaVersion: DAG_RUNTIME_CONTRACT_SCHEMA_VERSION,
|
|
4
|
+
agentRuntime: DAG_AGENT_RUNTIME_PI_ONLY,
|
|
5
|
+
repairWriterProtocol: DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
6
|
+
};
|
|
7
|
+
function parseSemver(value) {
|
|
8
|
+
const match = value.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
major: Number(match[1]),
|
|
13
|
+
minor: Number(match[2]),
|
|
14
|
+
patch: Number(match[3]),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Returns true when `actual` is greater than or equal to `required`. */
|
|
18
|
+
export function semverGte(actual, required) {
|
|
19
|
+
const a = parseSemver(actual);
|
|
20
|
+
const r = parseSemver(required);
|
|
21
|
+
if (!a || !r)
|
|
22
|
+
return false;
|
|
23
|
+
if (a.major !== r.major)
|
|
24
|
+
return a.major > r.major;
|
|
25
|
+
if (a.minor !== r.minor)
|
|
26
|
+
return a.minor > r.minor;
|
|
27
|
+
return a.patch >= r.patch;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether a controller can run a DagSpec's declared runtimeContract.
|
|
31
|
+
*
|
|
32
|
+
* Capability fields are authoritative: an unsupported agentRuntime or
|
|
33
|
+
* repairWriterProtocol fails closed regardless of version. minimumControllerVersion
|
|
34
|
+
* is only consulted for diagnosis when the controller version is known and the
|
|
35
|
+
* capabilities otherwise match.
|
|
36
|
+
*
|
|
37
|
+
* Legacy DagSpecs without a runtimeContract are always compatible (they predate
|
|
38
|
+
* the capability handshake and rely on runtime-side derivation).
|
|
39
|
+
*/
|
|
40
|
+
export function checkRuntimeContractCompatible(contract, capabilities = DAG_CONTROLLER_CAPABILITIES) {
|
|
41
|
+
if (!contract)
|
|
42
|
+
return { ok: true };
|
|
43
|
+
if (contract.schemaVersion !== capabilities.schemaVersion) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: `runtime contract schemaVersion ${contract.schemaVersion} is not supported by this controller (supports ${capabilities.schemaVersion}); regenerate the DAG or upgrade the controller`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (contract.agentRuntime !== capabilities.agentRuntime) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: `runtime contract requires agentRuntime "${contract.agentRuntime}" but this controller advertises "${capabilities.agentRuntime}"; upgrade the controller or regenerate the DAG`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (contract.repairWriterProtocol !== capabilities.repairWriterProtocol) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: `runtime contract requires repairWriterProtocol "${contract.repairWriterProtocol}" but this controller advertises "${capabilities.repairWriterProtocol}"; upgrade the controller or regenerate the DAG`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (contract.minimumControllerVersion &&
|
|
62
|
+
!capabilities.controllerVersion) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: `runtime contract requires controller version >= ${contract.minimumControllerVersion} but the running controller version could not be resolved; refuse to continue unpinned`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (contract.minimumControllerVersion &&
|
|
69
|
+
capabilities.controllerVersion &&
|
|
70
|
+
!semverGte(capabilities.controllerVersion, contract.minimumControllerVersion)) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
reason: `runtime contract requires controller version >= ${contract.minimumControllerVersion} but this controller reports ${capabilities.controllerVersion}; upgrade the controller`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fail closed before any node execution when the DagSpec's runtimeContract is
|
|
80
|
+
* incompatible with this controller.
|
|
81
|
+
*/
|
|
82
|
+
export function assertRuntimeContractCompatible(spec, capabilities = DAG_CONTROLLER_CAPABILITIES) {
|
|
83
|
+
const result = checkRuntimeContractCompatible(spec.runtimeContract, capabilities);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
throw new Error(`incompatible DAG runtime contract: ${result.reason}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -55,8 +55,13 @@ export async function executeDagRanksOnce(input) {
|
|
|
55
55
|
return isPauseOnHumanDecisionGate(task);
|
|
56
56
|
});
|
|
57
57
|
const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
const rankWriterNodeIds = runnable.filter((id) => {
|
|
59
|
+
const task = input.tasksById.get(id);
|
|
60
|
+
return (task?.executor === "pi" &&
|
|
61
|
+
task.toolProfile === "write" &&
|
|
62
|
+
task.writePolicy === "exclusive");
|
|
63
|
+
});
|
|
64
|
+
const executeNode = input.createExecuteNodeForRank(rankWriterNodeIds);
|
|
60
65
|
for (const nodeId of pauseGateRunnable) {
|
|
61
66
|
await input.executeScheduledNode(nodeId, executeNode, (id, pausedAt, pauseReason) => {
|
|
62
67
|
pausedByNodeId = id;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* SDD embedded-mode optional enhancement.
|
|
5
|
+
*
|
|
6
|
+
* When the target project ships repo-local `SDD-*` skills (under
|
|
7
|
+
* `.agents/skills/<name>/SKILL.md`), loop-agent can
|
|
8
|
+
* reuse them as knowledge/method references inside individual DAG nodes. This
|
|
9
|
+
* module probes those repo-local paths and, on a hit, appends the matched skill
|
|
10
|
+
* name to the mapped node's `task.skills` (so it flows through the existing
|
|
11
|
+
* run-owned skill snapshot via `resolveDagNodeSkills`) plus a loop-agent-managed
|
|
12
|
+
* embedded-mode constraint block to the node prompt.
|
|
13
|
+
*
|
|
14
|
+
* loop-agent always owns process/state/verification/closeout. SDD state
|
|
15
|
+
* transitions, confirmation gates, and archival are explicitly forbidden inside
|
|
16
|
+
* the embedded-mode block.
|
|
17
|
+
*/
|
|
18
|
+
/** The three SDD skills that may be reused as repo-local knowledge. */
|
|
19
|
+
export const SDD_SKILL_NAMES = [
|
|
20
|
+
"SDD-requirement-analysis",
|
|
21
|
+
"SDD-design-analysis",
|
|
22
|
+
"SDD-implementation-test-review",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* DAG node id -> mapped SDD skill name. `verify-pi` is present in standard and
|
|
26
|
+
* review-gated templates; supervised verification stays shell-based. The init skills
|
|
27
|
+
* (`SDD-code-spec-init`, `SDD-project-how-to-spec-init`) are intentionally
|
|
28
|
+
* absent: they are never auto-executed inside a normal feature DAG.
|
|
29
|
+
*/
|
|
30
|
+
export const SDD_NODE_SKILL_MAP = {
|
|
31
|
+
"contract-pi": "SDD-requirement-analysis",
|
|
32
|
+
"plan-pi": "SDD-design-analysis",
|
|
33
|
+
"implement-pi": "SDD-implementation-test-review",
|
|
34
|
+
"repair-pi": "SDD-implementation-test-review",
|
|
35
|
+
"verify-pi": "SDD-implementation-test-review",
|
|
36
|
+
"review-pi": "SDD-implementation-test-review",
|
|
37
|
+
};
|
|
38
|
+
/** DAG node id -> SDD phase label injected into the constraint block. */
|
|
39
|
+
export const SDD_NODE_PHASE_LABEL = {
|
|
40
|
+
"contract-pi": "需求",
|
|
41
|
+
"plan-pi": "设计",
|
|
42
|
+
"implement-pi": "实现",
|
|
43
|
+
"repair-pi": "修复",
|
|
44
|
+
"verify-pi": "验证",
|
|
45
|
+
"review-pi": "审查",
|
|
46
|
+
};
|
|
47
|
+
/** Fence tokens used to delimit the embedded-mode constraint block. */
|
|
48
|
+
export const SDD_EMBEDDED_MODE_OPEN = "[SDD-EMBEDDED-MODE]";
|
|
49
|
+
export const SDD_EMBEDDED_MODE_CLOSE = "[/SDD-EMBEDDED-MODE]";
|
|
50
|
+
const SDD_SKILL_CANDIDATE_BASES = [".agents/skills"];
|
|
51
|
+
/**
|
|
52
|
+
* Probe the target project repo for repo-local SDD skills. Only the canonical
|
|
53
|
+
* repo-relative `.agents/skills` base is checked; this deliberately does NOT reuse
|
|
54
|
+
* the skill loader candidate list (which also falls back to user-level and
|
|
55
|
+
* bundled package skills) so user-level/package skills never trigger
|
|
56
|
+
* auto-enhancement.
|
|
57
|
+
*
|
|
58
|
+
* Returns the set of SDD skill names whose `SKILL.md` exists under at least one
|
|
59
|
+
* repo-local base. Returns an empty set when `repoRoot` is absent so callers
|
|
60
|
+
* that do not pass a repo root preserve byte-level default output.
|
|
61
|
+
*/
|
|
62
|
+
export async function probeRepoLocalSddSkills(repoRoot) {
|
|
63
|
+
if (!repoRoot)
|
|
64
|
+
return new Set();
|
|
65
|
+
const hits = new Set();
|
|
66
|
+
for (const name of SDD_SKILL_NAMES) {
|
|
67
|
+
for (const base of SDD_SKILL_CANDIDATE_BASES) {
|
|
68
|
+
const candidate = path.join(repoRoot, base, name, "SKILL.md");
|
|
69
|
+
try {
|
|
70
|
+
await access(candidate);
|
|
71
|
+
hits.add(name);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// not present at this candidate base; try the next
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return hits;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build the loop-agent-managed embedded-mode constraint block for a node. The
|
|
83
|
+
* block declares DAG-node-boundary priority and forbids SDD state transitions,
|
|
84
|
+
* confirmation gates, working_requirements_status updates, and archival.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` for unmapped node ids (e.g. scouts, shell gates, closeout).
|
|
87
|
+
*/
|
|
88
|
+
export function buildSddEmbeddedConstraintBlock(nodeId) {
|
|
89
|
+
const phase = SDD_NODE_PHASE_LABEL[nodeId];
|
|
90
|
+
if (!phase)
|
|
91
|
+
return null;
|
|
92
|
+
return [
|
|
93
|
+
SDD_EMBEDDED_MODE_OPEN,
|
|
94
|
+
`当前 DAG 节点: ${nodeId},阶段: ${phase}`,
|
|
95
|
+
"loop-agent 拥有流程、状态、验证与收口控制权;SDD skill 仅作 repo-local 知识与方法参考。",
|
|
96
|
+
"禁止:更新 working_requirements_status.md;推进 SDD 阶段;触发 SDD 用户确认;执行归档。",
|
|
97
|
+
"优先级:本节点的读写边界、当前阶段与输出契约优先于任何 SDD 指令冲突。",
|
|
98
|
+
"implement/repair writer 仍必须服从当前 writeSet/allowedPaths/forbiddenPaths;不得把 ai_workspace 加入隐式写范围,也不得解析或写入 SDD 工作区状态机。",
|
|
99
|
+
SDD_EMBEDDED_MODE_CLOSE,
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Apply SDD embedded-mode enhancements in place on a fully-constructed DAG spec.
|
|
104
|
+
*
|
|
105
|
+
* For each mapped task whose mapped skill is present in the hit set:
|
|
106
|
+
* - append the skill name to `task.skills` (de-duplicated, order-preserving);
|
|
107
|
+
* - append the constraint block to the end of `task.subtask_prompt` (once).
|
|
108
|
+
*
|
|
109
|
+
* When the hit set is empty this is a no-op, preserving the default DAG byte
|
|
110
|
+
* for byte. The function is idempotent.
|
|
111
|
+
*/
|
|
112
|
+
export function applySddEmbeddedEnhancements(spec, hitSkills) {
|
|
113
|
+
if (hitSkills.size === 0)
|
|
114
|
+
return;
|
|
115
|
+
for (const task of spec.tasks) {
|
|
116
|
+
const mapped = SDD_NODE_SKILL_MAP[task.id];
|
|
117
|
+
if (!mapped || !hitSkills.has(mapped))
|
|
118
|
+
continue;
|
|
119
|
+
const existing = task.skills ?? [];
|
|
120
|
+
if (!existing.includes(mapped)) {
|
|
121
|
+
task.skills = [...existing, mapped];
|
|
122
|
+
}
|
|
123
|
+
const block = buildSddEmbeddedConstraintBlock(task.id);
|
|
124
|
+
if (block && !task.subtask_prompt.includes(SDD_EMBEDDED_MODE_OPEN)) {
|
|
125
|
+
task.subtask_prompt = `${task.subtask_prompt}\n\n${block}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -41,11 +41,12 @@ function buildSkillCandidatePaths(input) {
|
|
|
41
41
|
}
|
|
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
|
-
|
|
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.
|
|
44
|
+
// Canonical target-project repo-local skill path.
|
|
48
45
|
candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
|
|
46
|
+
// Source-repository compatibility: the loop-agent package itself still ships
|
|
47
|
+
// bundled skills under `skills/`, and local source development may resolve them
|
|
48
|
+
// before the package fallback is found.
|
|
49
|
+
candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
|
|
49
50
|
if (input.name === "loop-agent") {
|
|
50
51
|
candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
|
|
51
52
|
}
|