@tea-agent/loop-agent 0.10.0 → 0.11.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 +67 -25
- package/README.md +82 -11
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +23 -2
- package/dist/application/dag/run-dag.js +1 -27
- package/dist/application/dag/validate-dag.js +2 -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 +476 -91
- 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 +353 -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 +28 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +29 -2
- 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/read-model.js +27 -1
- package/dist/worker/observe/static/app.js +326 -45
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/styles.css +5 -4
- 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/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +402 -25
- package/dist/workflows/dag/node-execution.js +61 -7
- package/dist/workflows/dag/runner.js +45 -17
- 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 +527 -0
- package/dist/workflows/dag/types.js +42 -9
- package/dist/workflows/dag/validate.js +5 -8
- 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 +2 -2
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +134 -0
- package/docs/architecture/evolution.md +52 -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 +42 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +50 -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 +32 -1
- package/docs/reports/README.md +84 -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 +6 -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 +1 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -6
- 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 +13 -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 +15 -18
- 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/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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { taskExecutorSchema } from "../governance/manifest-types.js";
|
|
3
2
|
export const taskFlowSchema = z.enum([
|
|
4
3
|
"auto",
|
|
5
4
|
"micro",
|
|
@@ -8,7 +7,11 @@ export const taskFlowSchema = z.enum([
|
|
|
8
7
|
"loop",
|
|
9
8
|
"feature-study",
|
|
10
9
|
]);
|
|
11
|
-
export const taskKindSchema = z.enum([
|
|
10
|
+
export const taskKindSchema = z.enum([
|
|
11
|
+
"standard",
|
|
12
|
+
"feature-study",
|
|
13
|
+
"frontend-implementation",
|
|
14
|
+
]);
|
|
12
15
|
export const referenceRepoConfigSchema = z.object({
|
|
13
16
|
name: z.string().min(1),
|
|
14
17
|
path: z.string().min(1),
|
|
@@ -40,11 +43,13 @@ export const dagVerifyStrategySchema = z
|
|
|
40
43
|
.default("adapter"),
|
|
41
44
|
})
|
|
42
45
|
.optional();
|
|
43
|
-
export const
|
|
46
|
+
export const loopAutoExecutionPolicySchema = z.enum([
|
|
44
47
|
"off",
|
|
45
48
|
"approval-required",
|
|
46
49
|
"enabled",
|
|
47
50
|
]);
|
|
51
|
+
export const LOOP_AUTO_WRITE_POLICY_REMOVED_ERROR = "task field loopAutoWritePolicy is no longer supported; use loopAutoExecutionPolicy (off | approval-required | enabled)";
|
|
52
|
+
export const CURSOR_TASK_FIELD_REMOVED_ERROR = 'task fields "executor" and "cursorModel" are no longer supported; governed runtime is Pi-only';
|
|
48
53
|
export const convergenceConfigSchema = z.object({
|
|
49
54
|
enabled: z.boolean().optional().default(false),
|
|
50
55
|
maxPasses: z.number().int().positive().optional().default(3),
|
|
@@ -52,11 +57,11 @@ export const convergenceConfigSchema = z.object({
|
|
|
52
57
|
stopOnHardVerifyPass: z.boolean().optional().default(true),
|
|
53
58
|
pauseOnRegression: z.boolean().optional().default(true),
|
|
54
59
|
});
|
|
55
|
-
|
|
60
|
+
const taskConfigObjectSchema = z.object({
|
|
56
61
|
taskId: z.string(),
|
|
57
62
|
title: z.string(),
|
|
58
63
|
sourceFiles: z.array(z.string()),
|
|
59
|
-
/** standard: 本仓库需求实现;feature-study:
|
|
64
|
+
/** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 使用前端实现 DAG 模板 */
|
|
60
65
|
taskKind: taskKindSchema.optional().default("standard"),
|
|
61
66
|
referenceRepos: z.array(referenceRepoConfigSchema).optional().default([]),
|
|
62
67
|
referenceDocs: z.array(referenceDocConfigSchema).optional().default([]),
|
|
@@ -67,7 +72,7 @@ export const taskConfigSchema = z.object({
|
|
|
67
72
|
hardConstraints: z.array(z.string()).optional().default([]),
|
|
68
73
|
autoCommitAfterVerify: z.boolean().optional().default(true),
|
|
69
74
|
autoCommitMessage: z.string().optional().default(""),
|
|
70
|
-
/** Explicit reason that a medium/large task could not use DAG before loop
|
|
75
|
+
/** Explicit reason that a medium/large task could not use DAG before loop auto execution. */
|
|
71
76
|
dagFallbackReason: z.string().optional(),
|
|
72
77
|
/** full: attach governance bundle; slim: analyze/plan 只带最少仓库上下文(source + harness 等) */
|
|
73
78
|
contextProfile: contextProfileSchema.optional().default("full"),
|
|
@@ -90,8 +95,10 @@ export const taskConfigSchema = z.object({
|
|
|
90
95
|
maxFixLoops: z.number().int().min(0).optional().default(2),
|
|
91
96
|
/** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
|
|
92
97
|
convergence: convergenceConfigSchema.optional().default({ enabled: false }),
|
|
93
|
-
/** Outer loop auto mode never writes by default;
|
|
94
|
-
|
|
98
|
+
/** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
|
|
99
|
+
loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
|
|
100
|
+
.optional()
|
|
101
|
+
.default("off"),
|
|
95
102
|
requireRetrospective: z.boolean().optional().default(false),
|
|
96
103
|
/** inherit: use host shell env; clean: sanitized env for deterministic verify */
|
|
97
104
|
verifyEnv: z.enum(["inherit", "clean"]).optional().default("clean"),
|
|
@@ -99,9 +106,18 @@ export const taskConfigSchema = z.object({
|
|
|
99
106
|
maxGoalContinuationsPerRun: z.number().int().positive().optional().default(5),
|
|
100
107
|
/** Pi subagent assisted mode: 'off' (default), 'analyze-plan', or 'full' */
|
|
101
108
|
piSubagentMode: piSubagentModeSchema.optional().default("off"),
|
|
102
|
-
/** Leaf executor: default pi; cursor requires delegate + worktree isolation */
|
|
103
|
-
executor: taskExecutorSchema.optional().default("pi"),
|
|
104
|
-
/** Cursor model override (e.g. gpt-5.5); falls back to harness executors.cursor.defaultModel */
|
|
105
|
-
cursorModel: z.string().optional(),
|
|
106
109
|
notes: z.string().optional().default(""),
|
|
107
110
|
});
|
|
111
|
+
export const taskConfigSchema = z.preprocess((raw) => {
|
|
112
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
113
|
+
return raw;
|
|
114
|
+
}
|
|
115
|
+
const obj = raw;
|
|
116
|
+
if ("loopAutoWritePolicy" in obj) {
|
|
117
|
+
throw new Error(LOOP_AUTO_WRITE_POLICY_REMOVED_ERROR);
|
|
118
|
+
}
|
|
119
|
+
if ("executor" in obj || "cursorModel" in obj) {
|
|
120
|
+
throw new Error(CURSOR_TASK_FIELD_REMOVED_ERROR);
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
}, taskConfigObjectSchema);
|
package/dist/task/delegate.js
CHANGED
|
@@ -9,31 +9,24 @@ import { getTaskPaths, loadTaskConfig } from './runtime.js';
|
|
|
9
9
|
import { copyDir } from '../records/harvest.js';
|
|
10
10
|
import { saveWorkflowState } from './state.js';
|
|
11
11
|
import { workflowStateSchema } from '../shared/types.js';
|
|
12
|
-
import { assertCursorExecutorAvailable, resolveTaskExecutor, validateCursorTaskPreflight } from '../executors/config-core.js';
|
|
13
|
-
import { resolveDelegateExecutionMode } from '../executors/cursor-execution-log.js';
|
|
14
12
|
/**
|
|
15
13
|
* Atomic delegation: validate source → check conflicts → create worktree →
|
|
16
14
|
* sync source → optionally symlink node_modules.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
16
|
+
* Default is isolation only; callers may run Pi DAG via application use cases.
|
|
19
17
|
*/
|
|
20
18
|
export async function delegateTask(repoRoot, manifest, taskId, opts = {}) {
|
|
21
19
|
const symlinkEnabled = opts.symlinkNodeModules !== false;
|
|
22
20
|
await validateSourceMaterials(repoRoot, taskId);
|
|
23
|
-
|
|
21
|
+
await loadTaskConfig(repoRoot, taskId);
|
|
24
22
|
const effectiveManifest = manifest ?? (await loadHarnessManifest(repoRoot));
|
|
25
|
-
const executor = resolveTaskExecutor(taskConfig, opts.executor);
|
|
26
|
-
if (executor === 'cursor') {
|
|
27
|
-
assertCursorExecutorAvailable(effectiveManifest);
|
|
28
|
-
validateCursorTaskPreflight(taskConfig);
|
|
29
|
-
}
|
|
30
23
|
await validateNoConflicts(repoRoot, taskId, opts.branch);
|
|
31
24
|
const worktree = await createWorktree(repoRoot, effectiveManifest, {
|
|
32
25
|
taskId,
|
|
33
26
|
branch: opts.branch,
|
|
34
27
|
base: opts.base,
|
|
35
28
|
});
|
|
36
|
-
await syncSourceToWorktree(repoRoot, taskId, worktree.path
|
|
29
|
+
await syncSourceToWorktree(repoRoot, taskId, worktree.path);
|
|
37
30
|
let symlinkedNodeModules = false;
|
|
38
31
|
if (symlinkEnabled) {
|
|
39
32
|
symlinkedNodeModules = await setupNodeModulesSymlink(repoRoot, worktree.path);
|
|
@@ -46,13 +39,9 @@ export async function delegateTask(repoRoot, manifest, taskId, opts = {}) {
|
|
|
46
39
|
branch: worktree.branch,
|
|
47
40
|
baseBranch: worktree.base,
|
|
48
41
|
symlinkedNodeModules,
|
|
49
|
-
|
|
50
|
-
executionMode: resolveDelegateExecutionMode(executor),
|
|
42
|
+
executionMode: 'pi-worktree',
|
|
51
43
|
};
|
|
52
44
|
}
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Internal helpers
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
45
|
async function validateSourceMaterials(repoRoot, taskId) {
|
|
57
46
|
const { taskConfigPath, sourceDir } = getTaskPaths(repoRoot, taskId);
|
|
58
47
|
try {
|
|
@@ -96,7 +85,7 @@ async function validateNoConflicts(repoRoot, taskId, branch) {
|
|
|
96
85
|
console.error(`[delegate] warning: branch "${targetBranch}" already exists; will reuse it for the worktree.`);
|
|
97
86
|
}
|
|
98
87
|
}
|
|
99
|
-
async function syncSourceToWorktree(repoRoot, taskId, worktreePath
|
|
88
|
+
async function syncSourceToWorktree(repoRoot, taskId, worktreePath) {
|
|
100
89
|
const srcPaths = getTaskPaths(repoRoot, taskId);
|
|
101
90
|
const destPaths = getTaskPaths(worktreePath, taskId);
|
|
102
91
|
const destTaskDir = destPaths.taskDir;
|
|
@@ -106,16 +95,15 @@ async function syncSourceToWorktree(repoRoot, taskId, worktreePath, executor) {
|
|
|
106
95
|
await copyDir(srcPaths.sourceDir, destSourceDir);
|
|
107
96
|
const rawJson = await readFile(srcPaths.taskConfigPath, 'utf-8');
|
|
108
97
|
const parsedTaskConfig = JSON.parse(rawJson);
|
|
109
|
-
parsedTaskConfig.executor = executor;
|
|
110
98
|
await mkdir(path.dirname(destTaskConfigPath), { recursive: true });
|
|
111
99
|
await writeTaskConfig(worktreePath, taskId, parsedTaskConfig);
|
|
112
100
|
await mkdir(destPaths.logsDir, { recursive: true });
|
|
113
101
|
await initializeArtifacts(destTaskDir);
|
|
114
102
|
await initializeTaskLogStubs(worktreePath, taskId);
|
|
115
|
-
const state = await loadOrCreateWorkflowState(srcPaths.statePath, taskId
|
|
103
|
+
const state = await loadOrCreateWorkflowState(srcPaths.statePath, taskId);
|
|
116
104
|
await saveWorkflowState(destPaths.statePath, state);
|
|
117
105
|
}
|
|
118
|
-
async function loadOrCreateWorkflowState(statePath, taskId
|
|
106
|
+
async function loadOrCreateWorkflowState(statePath, taskId) {
|
|
119
107
|
try {
|
|
120
108
|
const raw = await readFile(statePath, 'utf-8');
|
|
121
109
|
return workflowStateSchema.parse(JSON.parse(raw));
|
|
@@ -129,8 +117,8 @@ async function loadOrCreateWorkflowState(statePath, taskId, executor) {
|
|
|
129
117
|
completedSteps: [],
|
|
130
118
|
lastUpdated: now,
|
|
131
119
|
executor: {
|
|
132
|
-
name:
|
|
133
|
-
mode:
|
|
120
|
+
name: 'pi',
|
|
121
|
+
mode: 'json',
|
|
134
122
|
},
|
|
135
123
|
artifacts: {
|
|
136
124
|
analysis: false,
|
package/dist/task/runtime.js
CHANGED
|
@@ -117,11 +117,10 @@ export async function createTask(repoRoot, taskId, title) {
|
|
|
117
117
|
stopOnHardVerifyPass: true,
|
|
118
118
|
pauseOnRegression: true,
|
|
119
119
|
},
|
|
120
|
-
|
|
120
|
+
loopAutoExecutionPolicy: 'off',
|
|
121
121
|
verifyEnv: 'clean',
|
|
122
122
|
maxGoalContinuationsPerRun: 5,
|
|
123
123
|
piSubagentMode: 'off',
|
|
124
|
-
executor: 'pi',
|
|
125
124
|
notes: '',
|
|
126
125
|
};
|
|
127
126
|
const state = {
|
package/dist/worker/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
+
import { readPackageVersion } from "../shared/package-metadata.js";
|
|
6
7
|
import YAML from "yaml";
|
|
7
8
|
import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
|
|
8
9
|
import { resolveLoopAgentProfile } from "./profile-mapping.js";
|
|
@@ -31,6 +32,7 @@ export function buildAgentWorkerProgram() {
|
|
|
31
32
|
program
|
|
32
33
|
.name("agent-worker")
|
|
33
34
|
.description("Local product-line worker utilities for TaskSpec validation")
|
|
35
|
+
.version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
|
|
34
36
|
.showHelpAfterError()
|
|
35
37
|
.showSuggestionAfterError();
|
|
36
38
|
const task = program.command("task").description("TaskSpec utilities");
|
|
@@ -66,6 +68,8 @@ export function buildAgentWorkerProgram() {
|
|
|
66
68
|
.option("--check-repo-command <command...>", "Override the check-repo preflight command")
|
|
67
69
|
.option("--quiet", "Suppress human-readable progress on stderr")
|
|
68
70
|
.option("--pi-model <model>", "Smoke override for pi executor nodes")
|
|
71
|
+
.option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
|
|
72
|
+
.option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
|
|
69
73
|
.description("Validate and advance one Ready Feature task through the existing Worker pipeline")
|
|
70
74
|
.action(async (options) => {
|
|
71
75
|
if (options.gitMode !== "none" && options.gitMode !== "checkpoint")
|
|
@@ -74,9 +78,11 @@ export function buildAgentWorkerProgram() {
|
|
|
74
78
|
throw new Error("--keep-failed-diff requires --git-mode checkpoint");
|
|
75
79
|
const repoRoot = path.resolve(options.repo);
|
|
76
80
|
const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
|
|
81
|
+
const expectedIdentity = buildIdentityExpectation(options);
|
|
77
82
|
const client = new LoopAgentClient({
|
|
78
83
|
loopAgentBin: options.loopAgentBin,
|
|
79
84
|
artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
|
|
85
|
+
resolveIdentity: true,
|
|
80
86
|
});
|
|
81
87
|
let progress;
|
|
82
88
|
try {
|
|
@@ -102,6 +108,8 @@ export function buildAgentWorkerProgram() {
|
|
|
102
108
|
...(options.piModel ? { piModel: options.piModel } : {}),
|
|
103
109
|
gitMode: options.gitMode === "checkpoint" ? "checkpoint" : "none",
|
|
104
110
|
keepFailedDiff: options.keepFailedDiff ?? false,
|
|
111
|
+
controllerIdentity: client.getIdentity(),
|
|
112
|
+
controllerExpectation: expectedIdentity,
|
|
105
113
|
});
|
|
106
114
|
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : renderFeatureRun(result));
|
|
107
115
|
if (result.status === "failed" || result.status === "needs-action")
|
|
@@ -114,11 +122,13 @@ export function buildAgentWorkerProgram() {
|
|
|
114
122
|
.requiredOption("--task-id <id>", "Completed qa-execute TaskSpec to rerun as dedicated final verification")
|
|
115
123
|
.option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
|
|
116
124
|
.option("--json", "Emit stable JSON")
|
|
125
|
+
.option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
|
|
126
|
+
.option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
|
|
117
127
|
.description("Run an independent HEAD-bound final QA verification and project Delivery evidence")
|
|
118
128
|
.action(async (options) => {
|
|
119
129
|
const repoRoot = path.resolve(options.repo);
|
|
120
|
-
const client = new LoopAgentClient({ loopAgentBin: options.loopAgentBin, artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `final-verification-${Date.now()}`) });
|
|
121
|
-
const result = await runFeatureFinalVerification({ featureDir: path.resolve(options.featureDir), repoRoot, taskId: options.taskId, client });
|
|
130
|
+
const client = new LoopAgentClient({ loopAgentBin: options.loopAgentBin, artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `final-verification-${Date.now()}`), resolveIdentity: true });
|
|
131
|
+
const result = await runFeatureFinalVerification({ featureDir: path.resolve(options.featureDir), repoRoot, taskId: options.taskId, client, controllerIdentity: client.getIdentity(), controllerExpectation: buildIdentityExpectation(options) });
|
|
122
132
|
process.stdout.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `Feature: ${result.featureId}\nFinal verification: ${result.workerRunId}\nQA evidence: ${result.qaEvidencePath}\nFinal evidence: ${result.finalVerificationPath}\n`);
|
|
123
133
|
});
|
|
124
134
|
feature
|
|
@@ -269,13 +279,17 @@ export function buildAgentWorkerProgram() {
|
|
|
269
279
|
.option("--check-repo-command <command...>", "Override the check-repo preflight command")
|
|
270
280
|
.option("--quiet", "Suppress human-readable progress on stderr (JSON still goes to stdout)")
|
|
271
281
|
.option("--pi-model <model>", "Smoke override: force every pi executor node to this model (drops --strict-models)")
|
|
282
|
+
.option("--expected-controller-version <version>", "Exact expected controller version (fail before writes if mismatched)")
|
|
283
|
+
.option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex> (fail before writes if mismatched)")
|
|
272
284
|
.description("Run ready TaskSpec tasks serially")
|
|
273
285
|
.action(async (options) => {
|
|
274
286
|
const repoRoot = path.resolve(options.repo);
|
|
275
287
|
const batchRunId = options.batchRunId ?? buildBatchRunId(new Date());
|
|
288
|
+
const expectedIdentity = buildIdentityExpectation(options);
|
|
276
289
|
const client = new LoopAgentClient({
|
|
277
290
|
loopAgentBin: options.loopAgentBin,
|
|
278
291
|
artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", batchRunId),
|
|
292
|
+
resolveIdentity: true,
|
|
279
293
|
});
|
|
280
294
|
let progress;
|
|
281
295
|
try {
|
|
@@ -301,6 +315,8 @@ export function buildAgentWorkerProgram() {
|
|
|
301
315
|
: {}),
|
|
302
316
|
progress,
|
|
303
317
|
...(options.piModel ? { piModel: options.piModel } : {}),
|
|
318
|
+
controllerIdentity: client.getIdentity(),
|
|
319
|
+
controllerExpectation: expectedIdentity,
|
|
304
320
|
});
|
|
305
321
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
306
322
|
if (result.status !== "completed")
|
|
@@ -365,6 +381,17 @@ export function buildAgentWorkerProgram() {
|
|
|
365
381
|
});
|
|
366
382
|
return program;
|
|
367
383
|
}
|
|
384
|
+
export function buildIdentityExpectation(options) {
|
|
385
|
+
if (options.expectedControllerVersion || options.expectedControllerFingerprint) {
|
|
386
|
+
const expectation = {};
|
|
387
|
+
if (options.expectedControllerVersion)
|
|
388
|
+
expectation.expectedVersion = options.expectedControllerVersion;
|
|
389
|
+
if (options.expectedControllerFingerprint)
|
|
390
|
+
expectation.expectedFingerprint = options.expectedControllerFingerprint;
|
|
391
|
+
return expectation;
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
368
395
|
export async function main(argv = process.argv) {
|
|
369
396
|
await buildAgentWorkerProgram().parseAsync(argv);
|
|
370
397
|
}
|
|
@@ -4,15 +4,29 @@ import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promis
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import YAML from "yaml";
|
|
7
|
+
import { controllerIdentitiesMatch, controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
7
8
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
8
9
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
9
10
|
import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
10
11
|
import { getRunsJsonlPath, getTaskPoolRoot, readJsonlFile, recordTaskPoolRun } from "../pool/run-store.js";
|
|
11
12
|
import { buildWorkerRunId, runTaskSpec } from "../run-task/run-task.js";
|
|
12
13
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
14
|
+
import { preflightTargetRepo } from "../preflight.js";
|
|
13
15
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
export async function runFeatureFinalVerification(input) {
|
|
18
|
+
const dependencies = {
|
|
19
|
+
preflight: preflightTargetRepo,
|
|
20
|
+
runTask: runTaskSpec,
|
|
21
|
+
git,
|
|
22
|
+
gitRaw,
|
|
23
|
+
...input.dependencies,
|
|
24
|
+
};
|
|
25
|
+
let controllerIdentity = resolveControllerIdentity(input.client, input.controllerIdentity);
|
|
26
|
+
const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, input.controllerExpectation);
|
|
27
|
+
if (identityFailure) {
|
|
28
|
+
throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
|
|
29
|
+
}
|
|
16
30
|
const repoRoot = await realpath(path.resolve(input.repoRoot));
|
|
17
31
|
const featureDir = await realpath(path.resolve(input.featureDir));
|
|
18
32
|
const relativeFeature = path.relative(repoRoot, featureDir);
|
|
@@ -22,10 +36,19 @@ export async function runFeatureFinalVerification(input) {
|
|
|
22
36
|
if (!validation.ok)
|
|
23
37
|
throw new Error(`Feature Packet is invalid: ${validation.errors.map((error) => error.code).join(", ")}`);
|
|
24
38
|
const featureId = validation.featureId;
|
|
39
|
+
const preflight = await dependencies.preflight({
|
|
40
|
+
repoRoot,
|
|
41
|
+
client: input.client,
|
|
42
|
+
...(input.controllerExpectation ? { expectation: input.controllerExpectation } : {}),
|
|
43
|
+
});
|
|
44
|
+
if (!preflight.ok) {
|
|
45
|
+
throw new Error(`target repo preflight failed: ${preflight.code}: ${preflight.message}`);
|
|
46
|
+
}
|
|
47
|
+
controllerIdentity = preflight.controllerIdentity ?? controllerIdentity;
|
|
25
48
|
const record = gitTransactionRecordSchema.parse(JSON.parse(await readFile(transactionRecordPath(repoRoot, featureId), "utf-8")));
|
|
26
|
-
const branch = await git(repoRoot, ["branch", "--show-current"]);
|
|
27
|
-
const headSha = await git(repoRoot, ["rev-parse", "HEAD"]);
|
|
28
|
-
const status = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
49
|
+
const branch = await dependencies.git(repoRoot, ["branch", "--show-current"]);
|
|
50
|
+
const headSha = await dependencies.git(repoRoot, ["rev-parse", "HEAD"]);
|
|
51
|
+
const status = await dependencies.gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
29
52
|
if (branch !== record.branch || headSha !== record.lastCheckpoint)
|
|
30
53
|
throw new Error("final verification requires the recorded Feature branch at lastCheckpoint");
|
|
31
54
|
if (status.trim())
|
|
@@ -50,17 +73,28 @@ export async function runFeatureFinalVerification(input) {
|
|
|
50
73
|
};
|
|
51
74
|
const now = input.now ?? new Date();
|
|
52
75
|
let runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
|
|
53
|
-
let finalRun = await reusableFinalRun(repoRoot, runs, taskSpec.id, featureId, record, now);
|
|
76
|
+
let finalRun = await reusableFinalRun(repoRoot, runs, taskSpec.id, featureId, record, now, controllerIdentity);
|
|
54
77
|
if (!finalRun) {
|
|
55
78
|
const workerRunId = buildWorkerRunId(`${input.taskId}-final`, now);
|
|
56
|
-
const result = await
|
|
79
|
+
const result = await dependencies.runTask({
|
|
80
|
+
repoRoot,
|
|
81
|
+
taskSpec: verificationTaskSpec,
|
|
82
|
+
taskSpecPath,
|
|
83
|
+
client: input.client,
|
|
84
|
+
now,
|
|
85
|
+
workerRunId,
|
|
86
|
+
skipSuccessFinalization: true,
|
|
87
|
+
preflight: false,
|
|
88
|
+
...(controllerIdentity ? { controllerIdentity } : {}),
|
|
89
|
+
...(input.controllerExpectation ? { controllerExpectation: input.controllerExpectation } : {}),
|
|
90
|
+
});
|
|
57
91
|
if (result.status !== "succeeded")
|
|
58
92
|
throw new Error(`dedicated final verification failed: ${workerRunId}`);
|
|
59
|
-
const afterHead = await git(repoRoot, ["rev-parse", "HEAD"]);
|
|
60
|
-
const afterStatus = await gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
93
|
+
const afterHead = await dependencies.git(repoRoot, ["rev-parse", "HEAD"]);
|
|
94
|
+
const afterStatus = await dependencies.gitRaw(repoRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
61
95
|
if (afterHead !== headSha || afterStatus.trim())
|
|
62
96
|
throw new Error("dedicated final verification changed the Delivery HEAD or worktree");
|
|
63
|
-
finalRun = { schemaVersion: 1, batchRunId: `final-verification-${workerRunId}`, workerRunId, taskId: taskSpec.id, featureId, status: "succeeded", harnessTaskId: result.harnessTaskId, runRecordPath: result.runRecordPath, dagPath: result.dagPath, recordedAt:
|
|
97
|
+
finalRun = { schemaVersion: 1, batchRunId: `final-verification-${workerRunId}`, workerRunId, taskId: taskSpec.id, featureId, status: "succeeded", harnessTaskId: result.harnessTaskId, runRecordPath: result.runRecordPath, dagPath: result.dagPath, recordedAt: now.toISOString(), ...(controllerIdentity ? { controllerIdentity } : {}) };
|
|
64
98
|
await recordTaskPoolRun({ repoRoot, run: finalRun });
|
|
65
99
|
runs = [...runs, finalRun];
|
|
66
100
|
}
|
|
@@ -87,7 +121,7 @@ export async function runFeatureFinalVerification(input) {
|
|
|
87
121
|
qa: { schemaVersion: 1, featureId, verdict: "passed", acIds: requiredAcIds, runs: qaRuns.map(runRef) },
|
|
88
122
|
final: { schemaVersion: 1, featureId, kind: "final-verification", status: "passed", headSha, run: runRef(finalRun), shellSummary: { path: summaryRelative, sha256: createHash("sha256").update(summary).digest("hex") } },
|
|
89
123
|
});
|
|
90
|
-
return { schemaVersion: 1, featureId, taskId: taskSpec.id, workerRunId: finalRun.workerRunId, headSha, qaEvidencePath: repoRef(repoRoot, qaEvidencePath), finalVerificationPath: repoRef(repoRoot, finalVerificationPath), qaRunCount: qaRuns.length };
|
|
124
|
+
return { schemaVersion: 1, featureId, taskId: taskSpec.id, workerRunId: finalRun.workerRunId, headSha, qaEvidencePath: repoRef(repoRoot, qaEvidencePath), finalVerificationPath: repoRef(repoRoot, finalVerificationPath), qaRunCount: qaRuns.length, ...(controllerIdentity ? { controllerIdentity } : {}) };
|
|
91
125
|
}
|
|
92
126
|
export function latestSuccessfulQaRuns(runs, specs, excludedTaskId, featureId) {
|
|
93
127
|
const byTask = new Map();
|
|
@@ -96,11 +130,13 @@ export function latestSuccessfulQaRuns(runs, specs, excludedTaskId, featureId) {
|
|
|
96
130
|
byTask.set(run.taskId, run);
|
|
97
131
|
return [...byTask.values()].sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
98
132
|
}
|
|
99
|
-
async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now) {
|
|
133
|
+
async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now, controllerIdentity) {
|
|
100
134
|
const latestCheckpointAt = Math.max(...record.checkpoints.map((entry) => new Date(entry.createdAt).getTime()));
|
|
101
135
|
for (const run of [...runs].reverse()) {
|
|
102
136
|
if (run.taskId !== taskId || run.featureId !== featureId || run.status !== "succeeded" || !run.workerRunId.includes("-final-") || !run.runRecordPath)
|
|
103
137
|
continue;
|
|
138
|
+
if (controllerIdentity && !controllerIdentitiesMatch(run.controllerIdentity, controllerIdentity))
|
|
139
|
+
continue;
|
|
104
140
|
const recordedAt = new Date(run.recordedAt).getTime();
|
|
105
141
|
if (recordedAt < latestCheckpointAt || now.getTime() - recordedAt > 24 * 60 * 60 * 1000)
|
|
106
142
|
continue;
|
|
@@ -117,7 +153,7 @@ async function reusableFinalRun(repoRoot, runs, taskId, featureId, record, now)
|
|
|
117
153
|
}
|
|
118
154
|
return undefined;
|
|
119
155
|
}
|
|
120
|
-
function runRef(run) { return { taskId: run.taskId, workerRunId: run.workerRunId, recordedAt: run.recordedAt }; }
|
|
156
|
+
function runRef(run) { return { taskId: run.taskId, workerRunId: run.workerRunId, recordedAt: run.recordedAt, ...(run.controllerIdentity ? { controllerIdentity: run.controllerIdentity } : {}) }; }
|
|
121
157
|
function repoRef(repoRoot, absolute) { return path.relative(repoRoot, absolute).replace(/\\/g, "/"); }
|
|
122
158
|
export async function writeEvidencePairAtomic(evidenceDir, value, fs = { rm }) {
|
|
123
159
|
const parent = path.dirname(evidenceDir);
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
|
|
8
9
|
import { getRunsJsonlPath, getTaskPoolRoot, readAllTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
9
10
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
10
11
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
@@ -34,10 +35,50 @@ export const deliveryManifestSchema = z.object({
|
|
|
34
35
|
riskSummary: z.object({ high: z.number().int().nonnegative(), medium: z.number().int().nonnegative(), low: z.number().int().nonnegative() }).strict(),
|
|
35
36
|
}).strict();
|
|
36
37
|
const waiverSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), items: z.array(z.object({ acId: z.string(), owner: z.string().min(1), reason: z.string().min(1), decidedAt: z.string().datetime() }).strict()) }).strict();
|
|
37
|
-
const
|
|
38
|
+
const controllerIdentityEvidenceSchema = z.object({
|
|
39
|
+
schemaVersion: z.literal(1),
|
|
40
|
+
packageName: z.literal(LOOP_AGENT_PACKAGE_NAME),
|
|
41
|
+
binName: z.string().min(1),
|
|
42
|
+
requested: z.string().min(1),
|
|
43
|
+
entry: z.string().min(1),
|
|
44
|
+
realEntry: z.string().min(1),
|
|
45
|
+
launch: z.object({ command: z.string().min(1), argsPrefix: z.array(z.string()) }).strict(),
|
|
46
|
+
binarySha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
47
|
+
packageRoot: z.string().min(1),
|
|
48
|
+
packageVersion: z.string().min(1),
|
|
49
|
+
reportedVersion: z.string().min(1).optional(),
|
|
50
|
+
packageFingerprint: z.object({
|
|
51
|
+
algorithm: z.literal("sha256"),
|
|
52
|
+
scopeVersion: z.literal(1),
|
|
53
|
+
scope: z.tuple([
|
|
54
|
+
z.literal("package.json"),
|
|
55
|
+
z.literal("bin/**"),
|
|
56
|
+
z.literal("dist/**"),
|
|
57
|
+
z.literal("skills/**"),
|
|
58
|
+
]),
|
|
59
|
+
value: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
60
|
+
fileCount: z.number().int().positive(),
|
|
61
|
+
}).strict(),
|
|
62
|
+
resolvedAt: z.string().datetime(),
|
|
63
|
+
}).strict().superRefine((identity, ctx) => {
|
|
64
|
+
if (identity.reportedVersion !== undefined
|
|
65
|
+
&& identity.reportedVersion !== identity.packageVersion) {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: z.ZodIssueCode.custom,
|
|
68
|
+
path: ["reportedVersion"],
|
|
69
|
+
message: "reportedVersion must match packageVersion",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const evidenceRunSchema = z.object({
|
|
74
|
+
taskId: z.string(),
|
|
75
|
+
workerRunId: z.string(),
|
|
76
|
+
recordedAt: z.string().datetime(),
|
|
77
|
+
controllerIdentity: controllerIdentityEvidenceSchema.optional(),
|
|
78
|
+
}).strict();
|
|
38
79
|
const qaEvidenceSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), verdict: z.literal("passed"), acIds: z.array(z.string()).min(1), runs: z.array(evidenceRunSchema).min(1) }).strict();
|
|
39
80
|
const finalVerificationSchema = z.object({ schemaVersion: z.literal(1), featureId: z.string(), kind: z.literal("final-verification"), status: z.literal("passed"), headSha: z.string().regex(/^[a-f0-9]{40}$/), run: evidenceRunSchema, shellSummary: hashedRefSchema }).strict();
|
|
40
|
-
const workerRunEvidenceSchema = z.object({ schemaVersion: z.literal(1), status: z.literal("succeeded"), workerRunId: z.string(), businessId: z.string(), featureId: z.string(), reportDecision: z.object({ succeeded: z.literal(true) }).passthrough(), commands: z.array(z.object({ name: z.string(), result: z.object({ ok: z.literal(true) }).passthrough() }).passthrough()).min(1) }).passthrough();
|
|
81
|
+
const workerRunEvidenceSchema = z.object({ schemaVersion: z.literal(1), status: z.literal("succeeded"), workerRunId: z.string(), businessId: z.string(), featureId: z.string(), reportDecision: z.object({ succeeded: z.literal(true) }).passthrough(), commands: z.array(z.object({ name: z.string(), result: z.object({ ok: z.literal(true) }).passthrough() }).passthrough()).min(1), controllerIdentity: controllerIdentityEvidenceSchema.optional() }).passthrough();
|
|
41
82
|
export async function prepareFeatureDelivery(input) {
|
|
42
83
|
const repoRoot = path.resolve(input.repoRoot);
|
|
43
84
|
const featureDir = path.resolve(input.featureDir);
|
|
@@ -300,8 +341,8 @@ async function canonicalQaEvidence(repoRoot, ref, featureId, runs, specs, now, b
|
|
|
300
341
|
if (data.featureId !== featureId)
|
|
301
342
|
throw new Error("Feature ownership mismatch");
|
|
302
343
|
for (const entry of data.runs) {
|
|
303
|
-
assertEvidenceRun(entry, featureId, runs, specs, now, true);
|
|
304
|
-
await readCanonicalWorkerRun(repoRoot,
|
|
344
|
+
const run = assertEvidenceRun(entry, featureId, runs, specs, now, true);
|
|
345
|
+
await readCanonicalWorkerRun(repoRoot, run);
|
|
305
346
|
}
|
|
306
347
|
const allowedAcIds = new Set(data.runs.flatMap((entry) => specs.get(entry.taskId)?.acceptance_refs ?? []));
|
|
307
348
|
for (const acId of data.acIds)
|
|
@@ -322,13 +363,13 @@ async function canonicalFinalEvidence(repoRoot, ref, featureId, runs, specs, rec
|
|
|
322
363
|
const data = finalVerificationSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, ref), "utf-8")));
|
|
323
364
|
if (data.featureId !== featureId)
|
|
324
365
|
throw new Error("Feature ownership mismatch");
|
|
325
|
-
assertEvidenceRun(data.run, featureId, runs, specs, now, true);
|
|
366
|
+
const taskPoolRun = assertEvidenceRun(data.run, featureId, runs, specs, now, true);
|
|
326
367
|
if (data.headSha !== record.lastCheckpoint)
|
|
327
368
|
throw new Error("final verification is not bound to Delivery HEAD");
|
|
328
369
|
const latestCheckpointAt = Math.max(...record.checkpoints.map((entry) => new Date(entry.createdAt).getTime()));
|
|
329
370
|
if (new Date(data.run.recordedAt).getTime() < latestCheckpointAt)
|
|
330
371
|
throw new Error("final verification predates the last checkpoint");
|
|
331
|
-
const runRecord = await readCanonicalWorkerRun(repoRoot,
|
|
372
|
+
const runRecord = await readCanonicalWorkerRun(repoRoot, taskPoolRun);
|
|
332
373
|
if (!runRecord.commands.some((command) => command.name === "run-dag"))
|
|
333
374
|
throw new Error("final verification run is missing a successful run-dag command");
|
|
334
375
|
const spec = specs.get(data.run.taskId);
|
|
@@ -360,6 +401,8 @@ function assertEvidenceRun(entry, featureId, runs, specs, now, requireQa) {
|
|
|
360
401
|
const age = now.getTime() - new Date(run.recordedAt).getTime();
|
|
361
402
|
if (age < 0 || age > 24 * 60 * 60 * 1000)
|
|
362
403
|
throw new Error(`run is not fresh within 24 hours: ${entry.workerRunId}`);
|
|
404
|
+
assertControllerIdentityPair(entry.controllerIdentity, run.controllerIdentity, "evidence and Task Pool run");
|
|
405
|
+
return run;
|
|
363
406
|
}
|
|
364
407
|
async function canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, blockers, workerRunId) {
|
|
365
408
|
const run = [...runs].reverse().find((candidate) => candidate.featureId === featureId && candidate.taskId === taskId && candidate.status === "succeeded" && candidate.runRecordPath && (!workerRunId || candidate.workerRunId === workerRunId));
|
|
@@ -369,7 +412,7 @@ async function canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, block
|
|
|
369
412
|
if (!hashed)
|
|
370
413
|
return undefined;
|
|
371
414
|
try {
|
|
372
|
-
await readCanonicalWorkerRun(repoRoot,
|
|
415
|
+
await readCanonicalWorkerRun(repoRoot, run);
|
|
373
416
|
return hashed;
|
|
374
417
|
}
|
|
375
418
|
catch (error) {
|
|
@@ -377,15 +420,25 @@ async function canonicalTaskRunEvidence(repoRoot, featureId, taskId, runs, block
|
|
|
377
420
|
return undefined;
|
|
378
421
|
}
|
|
379
422
|
}
|
|
380
|
-
async function readCanonicalWorkerRun(repoRoot,
|
|
381
|
-
const run = runs.find((candidate) => candidate.featureId === featureId && candidate.taskId === taskId && candidate.workerRunId === workerRunId && candidate.status === "succeeded" && candidate.runRecordPath);
|
|
423
|
+
async function readCanonicalWorkerRun(repoRoot, run) {
|
|
382
424
|
if (!run?.runRecordPath)
|
|
383
425
|
throw new Error("Task Pool run record path is missing");
|
|
384
426
|
const record = workerRunEvidenceSchema.parse(JSON.parse(await readFile(await safeRepoPath(repoRoot, run.runRecordPath), "utf-8")));
|
|
385
|
-
if (record.featureId !== featureId || record.businessId !== taskId || record.workerRunId !== workerRunId)
|
|
427
|
+
if (record.featureId !== run.featureId || record.businessId !== run.taskId || record.workerRunId !== run.workerRunId)
|
|
386
428
|
throw new Error("run record ownership mismatch");
|
|
429
|
+
assertControllerIdentityPair(run.controllerIdentity, record.controllerIdentity, "Task Pool run and worker run record");
|
|
387
430
|
return record;
|
|
388
431
|
}
|
|
432
|
+
function assertControllerIdentityPair(left, right, label) {
|
|
433
|
+
if (left === undefined && right === undefined)
|
|
434
|
+
return;
|
|
435
|
+
if (left === undefined || right === undefined)
|
|
436
|
+
throw new Error(`controller identity presence mismatch between ${label}`);
|
|
437
|
+
const parsedLeft = controllerIdentityEvidenceSchema.parse(left);
|
|
438
|
+
const parsedRight = controllerIdentityEvidenceSchema.parse(right);
|
|
439
|
+
if (!controllerIdentitiesMatch(parsedLeft, parsedRight))
|
|
440
|
+
throw new Error(`controller identity mismatch between ${label}`);
|
|
441
|
+
}
|
|
389
442
|
async function writeDeliveryArtifacts(artifacts, coverage, manifest) {
|
|
390
443
|
const deliveryDir = path.dirname(artifacts.manifest);
|
|
391
444
|
const parent = path.dirname(deliveryDir);
|