@tea-agent/loop-agent 0.16.25 → 0.17.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/CHANGELOG.md +46 -0
- package/README.md +14 -3
- package/dist/cli/command-definitions.js +43 -0
- package/dist/cli/program.js +26 -0
- package/dist/commands/dag-approve.js +4 -0
- package/dist/commands/dag-resume.js +1 -0
- package/dist/commands/dag-validate.js +6 -0
- package/dist/commands/operator.js +44 -0
- package/dist/commands/task-contract.js +271 -0
- package/dist/executors/dag-pi-executor.js +118 -16
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +85 -8
- package/dist/executors/shell-presets.js +16 -3
- package/dist/executors/shell-write-guard.js +64 -2
- package/dist/shared/operator/capabilities.js +255 -0
- package/dist/shared/operator/envelope.js +59 -0
- package/dist/shared/operator/index.js +4 -0
- package/dist/shared/operator/registry.js +38 -0
- package/dist/shared/operator/types.js +5 -0
- package/dist/task/contract/adopt.js +166 -0
- package/dist/task/contract/apply.js +326 -0
- package/dist/task/contract/canonicalize.js +60 -0
- package/dist/task/contract/constants.js +29 -0
- package/dist/task/contract/diff.js +177 -0
- package/dist/task/contract/hash.js +42 -0
- package/dist/task/contract/import-revision.js +96 -0
- package/dist/task/contract/index.js +17 -0
- package/dist/task/contract/journal.js +155 -0
- package/dist/task/contract/lock.js +153 -0
- package/dist/task/contract/observe.js +296 -0
- package/dist/task/contract/paths.js +19 -0
- package/dist/task/contract/project.js +170 -0
- package/dist/task/contract/recover.js +312 -0
- package/dist/task/contract/request-ledger.js +37 -0
- package/dist/task/contract/schema.js +151 -0
- package/dist/task/contract/transaction.js +160 -0
- package/dist/task/contract/types.js +1 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/index.js +3 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/runtime.js +5 -1
- package/dist/task/source-references.js +7 -0
- package/dist/worker/cli.js +150 -32
- package/dist/worker/console/app-data.js +185 -0
- package/dist/worker/console/dag-confirmation.js +313 -0
- package/dist/worker/console/doctor.js +169 -0
- package/dist/worker/console/draft-store.js +80 -0
- package/dist/worker/console/index.js +15 -0
- package/dist/worker/console/interview/assessment.js +67 -0
- package/dist/worker/console/interview/session.js +100 -0
- package/dist/worker/console/interview/tools.js +109 -0
- package/dist/worker/console/loopback.js +16 -0
- package/dist/worker/console/observe-health-match.js +174 -0
- package/dist/worker/console/observe-link.js +33 -0
- package/dist/worker/console/operation-runner.js +166 -0
- package/dist/worker/console/operation-sse.js +158 -0
- package/dist/worker/console/operation-store.js +147 -0
- package/dist/worker/console/operator-actions.js +769 -0
- package/dist/worker/console/pi-readiness.js +94 -0
- package/dist/worker/console/recovery-cta.js +133 -0
- package/dist/worker/console/repo-fingerprint.js +29 -0
- package/dist/worker/console/resource-loader.js +95 -0
- package/dist/worker/console/routes.js +368 -0
- package/dist/worker/console/security.js +126 -0
- package/dist/worker/console/server.js +149 -0
- package/dist/worker/console/sibling-controller.js +28 -0
- package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
- package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
- package/dist/worker/console/static/index.html +13 -0
- package/dist/worker/console/vite.config.js +27 -0
- package/dist/worker/delivery/git-transaction.js +43 -8
- package/dist/worker/observe/health.js +57 -0
- package/dist/worker/observe/paths.js +81 -0
- package/dist/worker/observe/routes.js +142 -27
- package/dist/worker/observe/spec-evidence.js +84 -0
- package/dist/worker/observe/static/api.js +23 -0
- package/dist/worker/observe/static/state.js +26 -0
- package/dist/worker/observe/static/styles.css +10 -0
- package/dist/worker/observe/static/views/dag-inspector.js +173 -6
- package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
- package/dist/workflows/dag/frontend-repair.js +1 -10
- package/dist/workflows/dag/init-hybrid.js +372 -115
- package/dist/workflows/dag/node-execution.js +57 -6
- package/dist/workflows/dag/project-governance-context.js +508 -0
- package/dist/workflows/dag/prompt.js +46 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +84 -10
- package/dist/workflows/dag/validate.js +53 -7
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +2 -0
- package/docs/architecture/system-overview.md +6 -0
- package/docs/architecture/worker-and-feature.md +7 -0
- package/docs/templates/agent-dag.schema.json +64 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
- package/docs/templates/backend-test-dag.json +26 -154
- package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
- package/package.json +8 -2
- package/skills/agent-worker/SKILL.md +1 -0
- package/skills/agent-worker/references/agent-worker-operator.md +3 -2
- package/skills/frontend-design-review/SKILL.md +25 -16
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/loop-agent/references/command-reference.md +48 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -4
|
@@ -98,8 +98,49 @@ function buildReadOnlyBoundary(writePolicy) {
|
|
|
98
98
|
"Return findings in the assistant response/stdout only; the DAG runner persists node artifacts under .harness/dag-runs/<state>/<run-id>/<node-id>/.",
|
|
99
99
|
];
|
|
100
100
|
}
|
|
101
|
+
function formatProjectGovernanceContext(ctx) {
|
|
102
|
+
if (!ctx || !ctx.applicable)
|
|
103
|
+
return undefined;
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push("Deterministic project governance manifest for the actual writer changeset of this run. Interpret it for compliance review; do not invent rules beyond the files and hashes listed here.");
|
|
106
|
+
lines.push("Before issuing a verdict, use read-only tools to read every applicable AGENTS.md and every mandatory referenced standard listed below from the repository; hashes bind the exact inputs for audit.");
|
|
107
|
+
lines.push("Your first non-empty output line must be exactly VERDICT: pass or VERDICT: request-revision. Request revision while any applicable mandatory instruction is violated.");
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("Change manifest (writer nodes and their changed files):");
|
|
110
|
+
for (const entry of ctx.changeManifest) {
|
|
111
|
+
if (entry.changedFiles.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
lines.push(`- ${entry.writerNodeId}: ${entry.changedFiles.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push("Applicable AGENTS.md chain (root -> nearest):");
|
|
117
|
+
for (const entry of ctx.agentsMdChain) {
|
|
118
|
+
lines.push(`- ${entry.path} (enforcement=mandatory, directory=${entry.directory || "/"}, sha256=${entry.sha256}, bytes=${entry.bytes}) applies to: ${entry.appliesTo.join(", ") || "(none)"}`);
|
|
119
|
+
}
|
|
120
|
+
if (ctx.referencedStandards.length > 0) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push("Referenced repository-local code standards:");
|
|
123
|
+
for (const std of ctx.referencedStandards) {
|
|
124
|
+
lines.push(`- ${std.path} (enforcement=${std.enforcement}, from ${ctx.agentsMdChain[std.fromAgentsMd]?.path ?? "AGENTS.md"}, sha256=${std.sha256}, bytes=${std.bytes}, scope=${std.scope})`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (ctx.unresolvedReferences.length > 0) {
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push("Unresolved reference diagnostics (structured, not read): for awareness only.");
|
|
130
|
+
for (const ref of ctx.unresolvedReferences) {
|
|
131
|
+
lines.push(`- ${ref.declaredPath}: ${ref.reason} (from ${ctx.agentsMdChain[ref.fromAgentsMd]?.path ?? "AGENTS.md"})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (ctx.diagnostics.length > 0) {
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("Resolver diagnostics:");
|
|
137
|
+
for (const diag of ctx.diagnostics)
|
|
138
|
+
lines.push(`- ${diag}`);
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
101
142
|
export function buildDagNodePromptEnvelope(input) {
|
|
102
|
-
const { spec, task, upstream, resolvedSkills = [], resolvedSkillInstructions = [], maxUpstreamChars = MAX_UPSTREAM_CHARS, } = input;
|
|
143
|
+
const { spec, task, upstream, resolvedSkills = [], resolvedSkillInstructions = [], maxUpstreamChars = MAX_UPSTREAM_CHARS, projectGovernanceContext, } = input;
|
|
103
144
|
const objective = spec.objective ?? spec.title;
|
|
104
145
|
const successCriteria = formatBulletList(spec.successCriteria, "(none specified)");
|
|
105
146
|
const globalConstraints = formatBulletList(spec.globalConstraints, "(none specified)");
|
|
@@ -150,6 +191,10 @@ export function buildDagNodePromptEnvelope(input) {
|
|
|
150
191
|
else {
|
|
151
192
|
sections.push("<upstream_context>\n(none)\n</upstream_context>");
|
|
152
193
|
}
|
|
194
|
+
const governanceSection = formatProjectGovernanceContext(projectGovernanceContext);
|
|
195
|
+
if (governanceSection) {
|
|
196
|
+
sections.push(`<project_governance_context>\n${governanceSection}\n</project_governance_context>`);
|
|
197
|
+
}
|
|
153
198
|
sections.push(`<task>\n${task.subtask_prompt}\n</task>`);
|
|
154
199
|
return sections.join("\n\n");
|
|
155
200
|
}
|
|
@@ -16,6 +16,11 @@ export const DEFAULT_DAG_RETRY_CATEGORIES = [
|
|
|
16
16
|
"rate-limit",
|
|
17
17
|
"unavailable",
|
|
18
18
|
];
|
|
19
|
+
export const STRUCTURED_OUTPUT_RETRY_CATEGORY = "output-too-large";
|
|
20
|
+
export const STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES = [
|
|
21
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
22
|
+
STRUCTURED_OUTPUT_RETRY_CATEGORY,
|
|
23
|
+
];
|
|
19
24
|
const RETRY_SAFE_PI_ROLES = new Set([
|
|
20
25
|
"planner",
|
|
21
26
|
"scout",
|
|
@@ -23,7 +28,7 @@ const RETRY_SAFE_PI_ROLES = new Set([
|
|
|
23
28
|
"verifier",
|
|
24
29
|
"closeout",
|
|
25
30
|
]);
|
|
26
|
-
export const dagRetryCategorySchema = z.enum(
|
|
31
|
+
export const dagRetryCategorySchema = z.enum(STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES);
|
|
27
32
|
export const dagRetryBackoffSchema = z.enum(["exponential"]);
|
|
28
33
|
/**
|
|
29
34
|
* Opt-in retry policy for a DagTask. Generated only for safe read-only Pi
|
|
@@ -71,6 +76,16 @@ export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
|
|
|
71
76
|
maxDelayMs: 30000,
|
|
72
77
|
retryCategories: [...DEFAULT_DAG_RETRY_CATEGORIES],
|
|
73
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Structured planner nodes may retry with a compact-output instruction
|
|
81
|
+
* when the model produced an oversized assistant response. This category is
|
|
82
|
+
* not part of the default read-only retry set because report/review nodes
|
|
83
|
+
* should not silently learn new output semantics.
|
|
84
|
+
*/
|
|
85
|
+
export const STRUCTURED_REQUIRED_PI_RETRY_POLICY = {
|
|
86
|
+
...DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
87
|
+
retryCategories: [...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES],
|
|
88
|
+
};
|
|
74
89
|
/**
|
|
75
90
|
* Deterministic helper: is this raw failure category eligible for retry under
|
|
76
91
|
* the given policy? Pure function; executor never decides retry eligibility.
|
|
@@ -117,6 +117,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
117
117
|
status: "PENDING",
|
|
118
118
|
executor: task.executor,
|
|
119
119
|
complexity: task.complexity,
|
|
120
|
+
...(task.outputMode ? { outputMode: task.outputMode } : {}),
|
|
120
121
|
...(resolveModelForTask(task, spec.executorModels)
|
|
121
122
|
? { model: resolveModelForTask(task, spec.executorModels) }
|
|
122
123
|
: {}),
|
|
@@ -178,6 +179,10 @@ export async function runDag(spec, opts) {
|
|
|
178
179
|
assertValidDagSpec(spec);
|
|
179
180
|
// Candidate identity is validated before run-id allocation or directory creation.
|
|
180
181
|
await assertEvaluationBindingPreflight(opts.cwd, spec.evaluation);
|
|
182
|
+
// Phase 0.5: new writer execution requires DagSpec v4 + live taskContractBinding match.
|
|
183
|
+
const { assertDagSpecAllowsNewWriterExecution, assertTaskContractBindingConsistent } = await import("./task-contract-binding.js");
|
|
184
|
+
assertDagSpecAllowsNewWriterExecution(spec);
|
|
185
|
+
await assertTaskContractBindingConsistent({ repoRoot: opts.cwd, spec });
|
|
181
186
|
const runningIdentity = resolveRunningControllerIdentity();
|
|
182
187
|
if (!runningIdentity) {
|
|
183
188
|
throw new Error("running controller identity could not be resolved; refuse to create an unpinned DAG run");
|
|
@@ -273,6 +278,10 @@ export async function resumeDagRun(opts) {
|
|
|
273
278
|
}
|
|
274
279
|
assertFrozenEvaluationBinding(spec, state);
|
|
275
280
|
assertFrozenBudget(spec.budget, state.budget, state.runId);
|
|
281
|
+
// Phase 0.5: refuse resume of pre-v4 writer DAGs; revalidate live binding.
|
|
282
|
+
const { assertDagSpecAllowsNewWriterExecution, assertTaskContractBindingConsistent } = await import("./task-contract-binding.js");
|
|
283
|
+
assertDagSpecAllowsNewWriterExecution(spec);
|
|
284
|
+
await assertTaskContractBindingConsistent({ repoRoot: opts.cwd, spec });
|
|
276
285
|
// Runtime contract + controller identity must be re-verified before executing
|
|
277
286
|
// any remaining node on resume; drift fails closed.
|
|
278
287
|
const runningIdentity = resolveRunningControllerIdentity();
|
|
@@ -524,6 +524,7 @@ export function buildNodePromptFromSnapshot(input) {
|
|
|
524
524
|
resolvedSkills: skillNames,
|
|
525
525
|
resolvedSkillInstructions,
|
|
526
526
|
maxUpstreamChars: policy.resolveMaxUpstreamChars(input.task),
|
|
527
|
+
projectGovernanceContext: input.projectGovernanceContext,
|
|
527
528
|
}),
|
|
528
529
|
resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
|
|
529
530
|
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
2
|
+
export async function readCurrentTaskContractBinding(input) {
|
|
3
|
+
const state = await observeTaskContract({
|
|
4
|
+
repoRoot: input.repoRoot,
|
|
5
|
+
taskId: input.taskId,
|
|
6
|
+
});
|
|
7
|
+
if (state.effectiveStatus !== "managed" || !state.ref) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
code: "BINDING_DRIFT",
|
|
11
|
+
message: `task ${input.taskId} is not managed (effectiveStatus=${state.effectiveStatus}); adopt/apply before DAG generate/run`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const ref = state.ref;
|
|
15
|
+
return {
|
|
16
|
+
ok: true,
|
|
17
|
+
current: {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
taskId: ref.taskId,
|
|
20
|
+
revision: ref.revision,
|
|
21
|
+
projectionVersion: ref.projectionVersion,
|
|
22
|
+
canonicalizerVersion: ref.canonicalizerVersion,
|
|
23
|
+
taskConfigSchemaVersion: ref.taskConfigSchemaVersion,
|
|
24
|
+
canonicalHash: ref.canonicalHash,
|
|
25
|
+
taskConfigSha256: ref.taskConfigSha256,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function compareTaskContractBinding(declared, current) {
|
|
30
|
+
if (declared.taskId !== current.taskId ||
|
|
31
|
+
declared.revision !== current.revision ||
|
|
32
|
+
declared.canonicalHash !== current.canonicalHash ||
|
|
33
|
+
declared.taskConfigSha256 !== current.taskConfigSha256 ||
|
|
34
|
+
declared.projectionVersion !== current.projectionVersion ||
|
|
35
|
+
declared.canonicalizerVersion !== current.canonicalizerVersion ||
|
|
36
|
+
declared.taskConfigSchemaVersion !== current.taskConfigSchemaVersion) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
code: "BINDING_DRIFT",
|
|
40
|
+
message: `taskContractBinding drift for ${declared.taskId}: declared revision=${declared.revision} hash=${declared.canonicalHash}, current revision=${current.revision} hash=${current.canonicalHash}`,
|
|
41
|
+
declared,
|
|
42
|
+
current,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, declared, current };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fail closed when a v4 DAG's frozen binding does not match current managed ref.
|
|
49
|
+
* Historical v3 specs without binding are left to migration matrix callers.
|
|
50
|
+
*/
|
|
51
|
+
export async function assertTaskContractBindingConsistent(input) {
|
|
52
|
+
if (input.spec.version !== 4) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const declared = input.spec.taskContractBinding;
|
|
56
|
+
if (!declared) {
|
|
57
|
+
const err = new Error("DagSpec version 4 missing taskContractBinding");
|
|
58
|
+
err.code = "BINDING_DRIFT";
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
const current = await readCurrentTaskContractBinding({
|
|
62
|
+
repoRoot: input.repoRoot,
|
|
63
|
+
taskId: declared.taskId,
|
|
64
|
+
});
|
|
65
|
+
if (!current.ok || !current.current) {
|
|
66
|
+
const err = new Error(current.message ?? "task contract binding unavailable");
|
|
67
|
+
err.code = current.code ?? "BINDING_DRIFT";
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const cmp = compareTaskContractBinding(declared, current.current);
|
|
71
|
+
if (!cmp.ok) {
|
|
72
|
+
const err = new Error(cmp.message ?? "taskContractBinding drift");
|
|
73
|
+
err.code = "BINDING_DRIFT";
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* v3 migration matrix: historical DAGs may remain readable/executable only
|
|
79
|
+
* when they contain no exclusive writer. Terminal report/doctor never call
|
|
80
|
+
* this helper.
|
|
81
|
+
*/
|
|
82
|
+
export function dagHasWriterExecution(spec) {
|
|
83
|
+
return spec.tasks.some((task) => (task.writePolicy ?? spec.defaults?.writePolicy) === "exclusive");
|
|
84
|
+
}
|
|
85
|
+
export function assertDagSpecAllowsNewWriterExecution(spec) {
|
|
86
|
+
if (!dagHasWriterExecution(spec))
|
|
87
|
+
return;
|
|
88
|
+
// Migration matrix (design §5.3.10):
|
|
89
|
+
// - New writer start/approve/resume requires DagSpec v4 + taskContractBinding.
|
|
90
|
+
// - Historical read-only/static/shell DAGs remain executable; they cannot
|
|
91
|
+
// introduce repository writer execution and remain useful for diagnostics,
|
|
92
|
+
// saved workflows, and deterministic verification.
|
|
93
|
+
// - v3 DAGs containing an exclusive writer must regenerate before execution.
|
|
94
|
+
if (spec.version !== 4) {
|
|
95
|
+
const err = new Error(`DagSpec version ${spec.version} cannot start/approve/resume new writer execution; regenerate as version 4 with taskContractBinding`);
|
|
96
|
+
err.code = "BINDING_DRIFT";
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Result-form API used by some callers; wraps assertTaskContractBindingConsistent. */
|
|
101
|
+
export async function assertTaskContractBindingFresh(input) {
|
|
102
|
+
const action = input.action ?? "dag-action";
|
|
103
|
+
if (input.rejectHistoricalV3ForWriter && input.spec.version === 3) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: "BINDING_DRIFT",
|
|
107
|
+
message: `${action}: DagSpec version 3 cannot start/approve/resume new writers; regenerate as v4`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (input.spec.version !== 4) {
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
binding: input.spec.taskContractBinding,
|
|
114
|
+
current: input.spec.taskContractBinding,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await assertTaskContractBindingConsistent({
|
|
119
|
+
repoRoot: input.repoRoot,
|
|
120
|
+
spec: input.spec,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
binding: input.spec.taskContractBinding,
|
|
125
|
+
current: input.spec.taskContractBinding,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const code = error && typeof error === "object" && "code" in error
|
|
130
|
+
? String(error.code)
|
|
131
|
+
: "BINDING_DRIFT";
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
code,
|
|
135
|
+
message: error instanceof Error ? error.message : String(error),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -7,6 +7,7 @@ export const dagNodeExecutorSchema = z.enum(["pi", "shell", "static"]);
|
|
|
7
7
|
export const CURSOR_DAG_EXECUTOR_REMOVED_ERROR = 'executor "cursor" is no longer supported; regenerate the DAG with Pi-only writers (implement-pi / repair-pi)';
|
|
8
8
|
export const CURSOR_EXECUTOR_MODELS_REMOVED_ERROR = "executorModels.cursor is no longer supported; use executorModels.pi only";
|
|
9
9
|
export const dagToolProfileSchema = z.enum(["read-only", "write"]);
|
|
10
|
+
export const dagOutputModeSchema = z.enum(["default", "structured-required"]);
|
|
10
11
|
export const dagShellPresetSchema = z.enum(["loop-agent-standard-verify"]);
|
|
11
12
|
export const dagVerifyQuotaSchema = z.enum(["1", "3", "full"]);
|
|
12
13
|
export const dagVerifyStrategySchema = z
|
|
@@ -75,6 +76,12 @@ export const dagRequirementCoverageGateSchema = z.object({
|
|
|
75
76
|
fromNodeIds: z
|
|
76
77
|
.array(z.string().regex(/^[a-z][a-z0-9-]*$/, "fromNodeIds must be kebab-case"))
|
|
77
78
|
.min(1),
|
|
79
|
+
fallbackFromNodeIds: z
|
|
80
|
+
.array(z
|
|
81
|
+
.string()
|
|
82
|
+
.regex(/^[a-z][a-z0-9-]*$/, "fallbackFromNodeIds must be kebab-case"))
|
|
83
|
+
.min(1)
|
|
84
|
+
.optional(),
|
|
78
85
|
requiredIds: z
|
|
79
86
|
.array(z
|
|
80
87
|
.string()
|
|
@@ -95,6 +102,12 @@ export const dagJsonArtifactSchemaIdSchema = z.enum([
|
|
|
95
102
|
]);
|
|
96
103
|
export const dagJsonArtifactGateSchema = z.object({
|
|
97
104
|
fromNodeId: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
105
|
+
fallbackFromNodeIds: z
|
|
106
|
+
.array(z
|
|
107
|
+
.string()
|
|
108
|
+
.regex(/^[a-z][a-z0-9-]*$/, "fallbackFromNodeIds must be kebab-case"))
|
|
109
|
+
.min(1)
|
|
110
|
+
.optional(),
|
|
98
111
|
schemaId: dagJsonArtifactSchemaIdSchema,
|
|
99
112
|
artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
|
|
100
113
|
outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
@@ -102,8 +115,25 @@ export const dagJsonArtifactGateSchema = z.object({
|
|
|
102
115
|
});
|
|
103
116
|
export const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
104
117
|
export const dagVersionSchema = z
|
|
105
|
-
.union([z.literal(1), z.literal(2), z.literal(3)])
|
|
118
|
+
.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)])
|
|
106
119
|
.default(1);
|
|
120
|
+
/** DagSpec v4 binding to managed TaskContractRefV1 (design §5.3.10). */
|
|
121
|
+
export const dagTaskContractBindingSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
schemaVersion: z.literal(1),
|
|
124
|
+
taskId: z.string().min(1),
|
|
125
|
+
revision: z.number().int().nonnegative(),
|
|
126
|
+
projectionVersion: z.literal(1),
|
|
127
|
+
canonicalizerVersion: z.literal(1),
|
|
128
|
+
taskConfigSchemaVersion: z.literal(1),
|
|
129
|
+
canonicalHash: z
|
|
130
|
+
.string()
|
|
131
|
+
.regex(/^[a-f0-9]{64}$/, "canonicalHash must be lowercase hex"),
|
|
132
|
+
taskConfigSha256: z
|
|
133
|
+
.string()
|
|
134
|
+
.regex(/^[a-f0-9]{64}$/, "taskConfigSha256 must be lowercase hex"),
|
|
135
|
+
})
|
|
136
|
+
.strict();
|
|
107
137
|
export const dagRoleSchema = z.enum([
|
|
108
138
|
"planner",
|
|
109
139
|
"scout",
|
|
@@ -152,6 +182,9 @@ export const dagShellConfigSchema = z.object({
|
|
|
152
182
|
commands: z.array(z.string()).default([]),
|
|
153
183
|
preset: dagShellPresetSchema.optional(),
|
|
154
184
|
verdictGate: dagVerdictGateSchema.optional(),
|
|
185
|
+
projectGovernanceGate: z.object({
|
|
186
|
+
contextPath: z.literal(".runtime/project-governance-context.json"),
|
|
187
|
+
}).strict().optional(),
|
|
155
188
|
requirementCoverageGate: dagRequirementCoverageGateSchema.optional(),
|
|
156
189
|
jsonArtifactGate: dagJsonArtifactGateSchema.optional(),
|
|
157
190
|
backendTestPipeline: dagBackendTestPipelineSchema.optional(),
|
|
@@ -310,7 +343,15 @@ export const dagTaskSchema = z.object({
|
|
|
310
343
|
shell: dagShellConfigSchema.optional(),
|
|
311
344
|
static: dagStaticConfigSchema.optional(),
|
|
312
345
|
outputContract: z.string().optional(),
|
|
346
|
+
outputMode: dagOutputModeSchema.optional(),
|
|
313
347
|
firstProtocolLine: z.string().min(1).optional(),
|
|
348
|
+
/**
|
|
349
|
+
* Explicit opt-in for the deterministic project governance context resolver
|
|
350
|
+
* (AGENTS.md chain + referenced code standards). Only tasks that set this
|
|
351
|
+
* to `true` receive a `<project_governance_context>` prompt section and
|
|
352
|
+
* closeout-blocking gate behavior. Never inferred from role/name.
|
|
353
|
+
*/
|
|
354
|
+
governanceStandardReview: z.boolean().optional(),
|
|
314
355
|
allowedPaths: z.array(z.string()).optional().default([]),
|
|
315
356
|
forbiddenPaths: z.array(z.string()).optional().default([]),
|
|
316
357
|
decisionGate: dagDecisionGateSchema.optional(),
|
|
@@ -366,9 +407,11 @@ export const dagSpecSchema = z
|
|
|
366
407
|
title: z.string().min(1),
|
|
367
408
|
runtimeContract: dagRuntimeContractSchema.optional(),
|
|
368
409
|
evaluation: dagEvaluationBindingSchema.optional(),
|
|
369
|
-
/** Optional hard/record-only budget; requires version 3. */
|
|
410
|
+
/** Optional hard/record-only budget; requires version 3 or 4. */
|
|
370
411
|
budget: dagBudgetSchema.optional(),
|
|
371
412
|
sourceBinding: dagSourceBindingSchema.optional(),
|
|
413
|
+
/** v4 managed Task Contract binding; only valid on version 4. */
|
|
414
|
+
taskContractBinding: dagTaskContractBindingSchema.optional(),
|
|
372
415
|
outputLanguage: dagOutputLanguageSchema.optional(),
|
|
373
416
|
objective: z.string().optional(),
|
|
374
417
|
successCriteria: z.array(z.string()).optional(),
|
|
@@ -381,34 +424,65 @@ export const dagSpecSchema = z
|
|
|
381
424
|
tasks: z.array(dagTaskSchema).min(1),
|
|
382
425
|
})
|
|
383
426
|
.superRefine((spec, ctx) => {
|
|
384
|
-
|
|
427
|
+
const supportsV3Fields = spec.version === 3 || spec.version === 4;
|
|
428
|
+
if (spec.evaluation && !supportsV3Fields) {
|
|
385
429
|
ctx.addIssue({
|
|
386
430
|
code: z.ZodIssueCode.custom,
|
|
387
|
-
message: "evaluation requires DagSpec version 3",
|
|
431
|
+
message: "evaluation requires DagSpec version 3 or 4",
|
|
388
432
|
path: ["evaluation"],
|
|
389
433
|
});
|
|
390
434
|
}
|
|
391
|
-
if (spec.budget &&
|
|
435
|
+
if (spec.budget && !supportsV3Fields) {
|
|
392
436
|
ctx.addIssue({
|
|
393
437
|
code: z.ZodIssueCode.custom,
|
|
394
|
-
message: "budget requires DagSpec version 3",
|
|
438
|
+
message: "budget requires DagSpec version 3 or 4",
|
|
395
439
|
path: ["budget"],
|
|
396
440
|
});
|
|
397
441
|
}
|
|
398
|
-
if (spec.runtimeContract &&
|
|
442
|
+
if (spec.runtimeContract && !supportsV3Fields) {
|
|
399
443
|
ctx.addIssue({
|
|
400
444
|
code: z.ZodIssueCode.custom,
|
|
401
|
-
message: "runtimeContract requires DagSpec version 3",
|
|
445
|
+
message: "runtimeContract requires DagSpec version 3 or 4",
|
|
402
446
|
path: ["version"],
|
|
403
447
|
});
|
|
404
448
|
}
|
|
405
|
-
if (
|
|
449
|
+
if (supportsV3Fields && !spec.runtimeContract) {
|
|
406
450
|
ctx.addIssue({
|
|
407
451
|
code: z.ZodIssueCode.custom,
|
|
408
|
-
message:
|
|
452
|
+
message: `DagSpec version ${spec.version} requires runtimeContract`,
|
|
409
453
|
path: ["runtimeContract"],
|
|
410
454
|
});
|
|
411
455
|
}
|
|
456
|
+
if (spec.taskContractBinding && spec.version !== 4) {
|
|
457
|
+
ctx.addIssue({
|
|
458
|
+
code: z.ZodIssueCode.custom,
|
|
459
|
+
message: "taskContractBinding requires DagSpec version 4",
|
|
460
|
+
path: ["taskContractBinding"],
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (spec.version === 4) {
|
|
464
|
+
if (!spec.sourceBinding) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: z.ZodIssueCode.custom,
|
|
467
|
+
message: "DagSpec version 4 requires sourceBinding",
|
|
468
|
+
path: ["sourceBinding"],
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (!spec.taskContractBinding) {
|
|
472
|
+
ctx.addIssue({
|
|
473
|
+
code: z.ZodIssueCode.custom,
|
|
474
|
+
message: "DagSpec version 4 requires taskContractBinding",
|
|
475
|
+
path: ["taskContractBinding"],
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (!spec.runtimeContract) {
|
|
479
|
+
ctx.addIssue({
|
|
480
|
+
code: z.ZodIssueCode.custom,
|
|
481
|
+
message: "DagSpec version 4 requires runtimeContract",
|
|
482
|
+
path: ["runtimeContract"],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
412
486
|
// Catch raw cursor keys that Zod .strict() on nested objects already rejects when parsed
|
|
413
487
|
// via parseDagSpec; this refine covers typed object construction paths.
|
|
414
488
|
const models = spec.executorModels;
|
|
@@ -256,10 +256,20 @@ function validateRequirementCoverageGateConfig(task, spec, issues) {
|
|
|
256
256
|
issues.push({ type: "invalid-requirement-coverage-gate-config", message: `task ${task.id} shell.requirementCoverageGate requires executor=shell` });
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
|
+
if (gate.fallbackFromNodeIds?.length && gate.fromNodeIds.length !== 1) {
|
|
260
|
+
issues.push({
|
|
261
|
+
type: "invalid-requirement-coverage-gate-config",
|
|
262
|
+
message: `task ${task.id} shell.requirementCoverageGate with fallbackFromNodeIds requires exactly one fromNodeIds entry`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
259
265
|
const dependencyIds = new Set(task.depends_on);
|
|
260
|
-
|
|
266
|
+
const sourceNodeIds = [
|
|
267
|
+
...gate.fromNodeIds,
|
|
268
|
+
...(gate.fallbackFromNodeIds ?? []),
|
|
269
|
+
];
|
|
270
|
+
for (const fromNodeId of new Set(sourceNodeIds)) {
|
|
261
271
|
if (!dependencyIds.has(fromNodeId)) {
|
|
262
|
-
issues.push({ type: "invalid-requirement-coverage-gate-config", message: `task ${task.id} shell.requirementCoverageGate
|
|
272
|
+
issues.push({ type: "invalid-requirement-coverage-gate-config", message: `task ${task.id} shell.requirementCoverageGate source "${fromNodeId}" must appear in depends_on` });
|
|
263
273
|
}
|
|
264
274
|
const upstream = spec.tasks.find((candidate) => candidate.id === fromNodeId);
|
|
265
275
|
if (upstream?.executor === "shell") {
|
|
@@ -493,11 +503,19 @@ function validateShellTaskConfig(task, spec, issues) {
|
|
|
493
503
|
}
|
|
494
504
|
validateVerdictGateConfig(task, spec, issues);
|
|
495
505
|
validateRequirementCoverageGateConfig(task, spec, issues);
|
|
496
|
-
if (shell.jsonArtifactGate
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
506
|
+
if (shell.jsonArtifactGate) {
|
|
507
|
+
const sourceNodeIds = [
|
|
508
|
+
shell.jsonArtifactGate.fromNodeId,
|
|
509
|
+
...(shell.jsonArtifactGate.fallbackFromNodeIds ?? []),
|
|
510
|
+
];
|
|
511
|
+
for (const sourceNodeId of new Set(sourceNodeIds)) {
|
|
512
|
+
if (!task.depends_on.includes(sourceNodeId)) {
|
|
513
|
+
issues.push({
|
|
514
|
+
type: "missing-dependency",
|
|
515
|
+
message: `shell task ${task.id} jsonArtifactGate source ${sourceNodeId} must be a direct dependency`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
501
519
|
}
|
|
502
520
|
validateRepairArtifactGateConfig(task, spec, issues);
|
|
503
521
|
validateShellVerdictGateGovernance(task, commands, issues);
|
|
@@ -653,12 +671,40 @@ export function validateDagSpec(spec) {
|
|
|
653
671
|
validateStaticTaskConfig(task, issues);
|
|
654
672
|
validateDecisionGateTaskConfig(task, issues);
|
|
655
673
|
validateRetryPolicyTaskConfig(task, issues);
|
|
674
|
+
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
656
675
|
}
|
|
657
676
|
validateSameRankWriteSetConflicts(spec, ranks, issues);
|
|
658
677
|
validateSameRankAgentAttributionRisks(spec, ranks, issues);
|
|
659
678
|
validateWriterSourceBinding(spec, issues);
|
|
660
679
|
return issues;
|
|
661
680
|
}
|
|
681
|
+
function validateProjectGovernanceTaskConfig(task, spec, issues) {
|
|
682
|
+
if (task.governanceStandardReview) {
|
|
683
|
+
if (task.executor !== "pi" ||
|
|
684
|
+
task.toolProfile === "write" ||
|
|
685
|
+
!["read-only", "none"].includes((task.writePolicy ?? "read-only"))) {
|
|
686
|
+
issues.push({
|
|
687
|
+
type: "invalid-project-governance-config",
|
|
688
|
+
message: `task ${task.id} governanceStandardReview requires a read-only Pi node`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (!task.shell?.projectGovernanceGate)
|
|
693
|
+
return;
|
|
694
|
+
const verdictGate = task.shell.verdictGate;
|
|
695
|
+
const source = verdictGate
|
|
696
|
+
? spec.tasks.find((candidate) => candidate.id === verdictGate.fromNodeId)
|
|
697
|
+
: undefined;
|
|
698
|
+
if (task.executor !== "shell" ||
|
|
699
|
+
!verdictGate ||
|
|
700
|
+
!task.depends_on.includes(verdictGate.fromNodeId) ||
|
|
701
|
+
!source?.governanceStandardReview) {
|
|
702
|
+
issues.push({
|
|
703
|
+
type: "invalid-project-governance-config",
|
|
704
|
+
message: `task ${task.id} projectGovernanceGate requires verdictGate over a directly-dependent governanceStandardReview node`,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
662
708
|
export function assertValidDagSpec(spec, options = {}) {
|
|
663
709
|
const issues = collectBlockingIssues(options.issues ?? validateDagSpec(spec), options);
|
|
664
710
|
if (issues.length > 0) {
|
package/docs/README.md
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
- `design/frontend-mock-data-workflow.md` — 已实现的前端 Mock 数据节点、触发条件、规范证据、验证与失败路由
|
|
32
32
|
- `design/backend-test-workflow.md` — 已实现的 backend-test 15 节点单次执行全流程、pass-only 评审门禁、run-owned artifacts 与最终 outcome
|
|
33
33
|
- `design/dag-source-binding-and-recovery.md` — 新生成 DAG 的权威任务源绑定、前端需求编号覆盖门禁与中断恢复规则
|
|
34
|
+
- `design/agent-worker-fullstack-workflow-integration.md` — 已实现的 workflow routing、Task Outcome、artifact-aware Ready、`fullstack-v1` 与 Verification Bundle,以及后续 failure routing / execute-existing 领域设计
|
|
35
|
+
- `design/fullstack-end-to-end-delivery-optimization-roadmap.md` — 当前全栈端到端优化的收敛路线图:先完成 release train / Delivery,再冻结 Final Verification 权威、Environment Contract、分类恢复与 Observe 指标
|
|
34
36
|
- `cursor-prompt-sidecar.md` — `cursor-prompt` one-shot sidecar 用法(非受治理 writer)
|
|
35
37
|
- `init-surface.manifest.json` — npm 包范围、目标项目初始化投影与 `init check-update` surface 分类的机器校验契约
|
|
36
38
|
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
| Loop 与 Dynamic Workflow 更深的双向集成、稳定化与自动恢复 | 设计输入 | 同上;当前已有基础 `workflow` action,不应误写为完全缺失 |
|
|
41
41
|
|
|
42
42
|
> 本地 Loop Operator Console 已在 `docs/design/local-operator-console-from-pi-web.md` 作为独立设计输入:单仓库、loopback、随 `@tea-agent/loop-agent` 同包发布、canonical mutation 只经 sibling CLI;它不是本表中的远端 Web Console,也不能把远端多租户/云编排需求偷渡进本地 MVP。
|
|
43
|
+
>
|
|
44
|
+
> **Phase 3 落地(Unreleased)**:versioned Observe `/api/health`、Console `observeLink` fail-closed、recovery CTA 矩阵;Console 与 Observe 仍分进程。Phase 4 General Operator Chat 与合服仍属后续,未实现。
|
|
43
45
|
|
|
44
46
|
> 注意:`ai_workspace/loop-agent/design/dynamic-workflow-dag-engine-roadmap.md` 是 2026-07-04 历史叙述;文中凡把 Cursor 写成受治理 executor 或 `loop` 的 `cursor-fix` 动作,均为**历史叙述**,现状以 Pi-only + 显式 `cursor-prompt` sidecar 为准。
|
|
45
47
|
|
|
@@ -59,6 +59,12 @@ workflow runtime(调度 Pi / shell / static 节点)
|
|
|
59
59
|
|
|
60
60
|
`agent-worker` 位于核心 runtime 的**上游调用侧**,不是 DAG 执行完成后的必经下游。Observe 可以在任一路径后读取现有事实,但不会改变执行结果。
|
|
61
61
|
|
|
62
|
+
### 本地 Operator Console(Official,与 Observe 协作)
|
|
63
|
+
|
|
64
|
+
- `agent-worker console serve` 提供 loopback Operator Console;canonical mutation 只经 sibling 已发布 `loop-agent`(`LoopAgentClient`),不 in-process 跑 DAG kernel。
|
|
65
|
+
- Observe 保持**独立**只读进程;Console 通过 versioned Observe `/api/health`(`schemaVersion`、`repoFingerprint`、route capabilities)做深链 fail-closed,**不** mount / proxy。
|
|
66
|
+
- openCode 等主会话仍是 Compatibility / Operator Assist,与 Official Console **不等同**。设计锚点:ADR 0005、`docs/design/local-operator-console-from-pi-web.md`。
|
|
67
|
+
|
|
62
68
|
## 外部边界
|
|
63
69
|
|
|
64
70
|
- npm 发布包:`@tea-agent/loop-agent`,包含两个 bin 与静态能力资料。
|
|
@@ -73,6 +73,13 @@ controller identity 与 DAG skill snapshot 是两个不同冻结层(前者跨
|
|
|
73
73
|
- 模块:`src/worker/observe/`、`src/worker/observability/{read-model,event-store}.ts`。
|
|
74
74
|
- 全局快照:`buildGlobalSnapshot({ repoRoot })`(`src/worker/observability/read-model.ts`),是 **derived** 视图,消费 `.harness/` 与 Task Pool 事实,**不**改变执行成败。
|
|
75
75
|
- Observe 是本地只读暖白控制台;snapshot 投影失败返回安全错误摘要而非全零健康状态(见 `CHANGELOG.md [0.9.0]`)。
|
|
76
|
+
- Phase 3:`GET /api/health` 为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package 元数据、由 ROUTES 派生的 `routeCapabilities`)。Console 深链 fail-closed 依赖该合同;Observe 仍独立进程、不经 Console proxy。
|
|
77
|
+
|
|
78
|
+
### Loop Operator Console(Official 控制面骨架)
|
|
79
|
+
|
|
80
|
+
- 模块:`src/worker/console/`(loopback serve、doctor、operator API、Vite SPA)。
|
|
81
|
+
- 与 Observe 协作:`observeLink` + recovery CTA 矩阵;无 Cancel / 无主 CTA「直接改代码」。
|
|
82
|
+
- 写入路径仍只经 `LoopAgentClient` → 已发布 `loop-agent`;不与 Observe 合服。
|
|
76
83
|
|
|
77
84
|
## 版本化自举的 deterministic canary
|
|
78
85
|
|