@tea-agent/loop-agent 0.16.26 → 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 +38 -1
- 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 +55 -7
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +55 -8
- package/dist/executors/shell-presets.js +16 -3
- 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/observe/health.js +57 -0
- package/dist/worker/observe/routes.js +16 -9
- 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 +269 -116
- package/dist/workflows/dag/node-execution.js +25 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +74 -10
- package/dist/workflows/dag/validate.js +25 -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 +49 -2
- 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
|
@@ -25,6 +25,23 @@ export function buildNodePrompt(spec, task, upstream, options) {
|
|
|
25
25
|
projectGovernanceContext: options?.projectGovernanceContext,
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
+
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory) {
|
|
29
|
+
if (attemptNumber <= 1 ||
|
|
30
|
+
task.outputMode !== "structured-required" ||
|
|
31
|
+
previousFailureCategory !== "output-too-large") {
|
|
32
|
+
return basePrompt;
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
basePrompt,
|
|
36
|
+
"",
|
|
37
|
+
"<retry_instruction>",
|
|
38
|
+
"Previous attempt exceeded the structured output size limit.",
|
|
39
|
+
"Return only the compact structured artifact required by this node's output contract.",
|
|
40
|
+
"Do not include explanatory prose, duplicated upstream context, long evidence excerpts, or additional markdown sections.",
|
|
41
|
+
"If a fenced JSON object is required, output exactly one fenced json block and nothing else.",
|
|
42
|
+
"</retry_instruction>",
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
28
45
|
export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
|
|
29
46
|
const policy = resolveContextPolicy(spec);
|
|
30
47
|
const skillNames = policy.resolveSkills(spec, task);
|
|
@@ -283,6 +300,7 @@ export async function executeDagNode(input) {
|
|
|
283
300
|
const attempts = [];
|
|
284
301
|
let totalBackoffMs = 0;
|
|
285
302
|
let terminalResult;
|
|
303
|
+
let previousFailureCategory;
|
|
286
304
|
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
287
305
|
const attemptStartedAt = new Date().toISOString();
|
|
288
306
|
const attemptStarted = Date.now();
|
|
@@ -293,7 +311,12 @@ export async function executeDagNode(input) {
|
|
|
293
311
|
tasksById,
|
|
294
312
|
state,
|
|
295
313
|
});
|
|
296
|
-
result = await executeNode({
|
|
314
|
+
result = await executeNode({
|
|
315
|
+
task,
|
|
316
|
+
cwd,
|
|
317
|
+
model,
|
|
318
|
+
prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory),
|
|
319
|
+
});
|
|
297
320
|
}
|
|
298
321
|
catch (error) {
|
|
299
322
|
result = {
|
|
@@ -356,6 +379,7 @@ export async function executeDagNode(input) {
|
|
|
356
379
|
await input.persistState();
|
|
357
380
|
}
|
|
358
381
|
terminalResult = result;
|
|
382
|
+
previousFailureCategory = result.failureCategory;
|
|
359
383
|
if (result.ok)
|
|
360
384
|
break;
|
|
361
385
|
const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
|
|
@@ -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();
|
|
@@ -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",
|
|
@@ -313,6 +343,7 @@ export const dagTaskSchema = z.object({
|
|
|
313
343
|
shell: dagShellConfigSchema.optional(),
|
|
314
344
|
static: dagStaticConfigSchema.optional(),
|
|
315
345
|
outputContract: z.string().optional(),
|
|
346
|
+
outputMode: dagOutputModeSchema.optional(),
|
|
316
347
|
firstProtocolLine: z.string().min(1).optional(),
|
|
317
348
|
/**
|
|
318
349
|
* Explicit opt-in for the deterministic project governance context resolver
|
|
@@ -376,9 +407,11 @@ export const dagSpecSchema = z
|
|
|
376
407
|
title: z.string().min(1),
|
|
377
408
|
runtimeContract: dagRuntimeContractSchema.optional(),
|
|
378
409
|
evaluation: dagEvaluationBindingSchema.optional(),
|
|
379
|
-
/** Optional hard/record-only budget; requires version 3. */
|
|
410
|
+
/** Optional hard/record-only budget; requires version 3 or 4. */
|
|
380
411
|
budget: dagBudgetSchema.optional(),
|
|
381
412
|
sourceBinding: dagSourceBindingSchema.optional(),
|
|
413
|
+
/** v4 managed Task Contract binding; only valid on version 4. */
|
|
414
|
+
taskContractBinding: dagTaskContractBindingSchema.optional(),
|
|
382
415
|
outputLanguage: dagOutputLanguageSchema.optional(),
|
|
383
416
|
objective: z.string().optional(),
|
|
384
417
|
successCriteria: z.array(z.string()).optional(),
|
|
@@ -391,34 +424,65 @@ export const dagSpecSchema = z
|
|
|
391
424
|
tasks: z.array(dagTaskSchema).min(1),
|
|
392
425
|
})
|
|
393
426
|
.superRefine((spec, ctx) => {
|
|
394
|
-
|
|
427
|
+
const supportsV3Fields = spec.version === 3 || spec.version === 4;
|
|
428
|
+
if (spec.evaluation && !supportsV3Fields) {
|
|
395
429
|
ctx.addIssue({
|
|
396
430
|
code: z.ZodIssueCode.custom,
|
|
397
|
-
message: "evaluation requires DagSpec version 3",
|
|
431
|
+
message: "evaluation requires DagSpec version 3 or 4",
|
|
398
432
|
path: ["evaluation"],
|
|
399
433
|
});
|
|
400
434
|
}
|
|
401
|
-
if (spec.budget &&
|
|
435
|
+
if (spec.budget && !supportsV3Fields) {
|
|
402
436
|
ctx.addIssue({
|
|
403
437
|
code: z.ZodIssueCode.custom,
|
|
404
|
-
message: "budget requires DagSpec version 3",
|
|
438
|
+
message: "budget requires DagSpec version 3 or 4",
|
|
405
439
|
path: ["budget"],
|
|
406
440
|
});
|
|
407
441
|
}
|
|
408
|
-
if (spec.runtimeContract &&
|
|
442
|
+
if (spec.runtimeContract && !supportsV3Fields) {
|
|
409
443
|
ctx.addIssue({
|
|
410
444
|
code: z.ZodIssueCode.custom,
|
|
411
|
-
message: "runtimeContract requires DagSpec version 3",
|
|
445
|
+
message: "runtimeContract requires DagSpec version 3 or 4",
|
|
412
446
|
path: ["version"],
|
|
413
447
|
});
|
|
414
448
|
}
|
|
415
|
-
if (
|
|
449
|
+
if (supportsV3Fields && !spec.runtimeContract) {
|
|
416
450
|
ctx.addIssue({
|
|
417
451
|
code: z.ZodIssueCode.custom,
|
|
418
|
-
message:
|
|
452
|
+
message: `DagSpec version ${spec.version} requires runtimeContract`,
|
|
419
453
|
path: ["runtimeContract"],
|
|
420
454
|
});
|
|
421
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
|
+
}
|
|
422
486
|
// Catch raw cursor keys that Zod .strict() on nested objects already rejects when parsed
|
|
423
487
|
// via parseDagSpec; this refine covers typed object construction paths.
|
|
424
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);
|
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
|
|
|
@@ -255,12 +255,54 @@
|
|
|
255
255
|
"requirementCoverageGate": {
|
|
256
256
|
"type": "object", "additionalProperties": false,
|
|
257
257
|
"required": ["fromNodeIds", "requiredIds"],
|
|
258
|
+
"allOf": [
|
|
259
|
+
{
|
|
260
|
+
"if": { "required": ["fallbackFromNodeIds"] },
|
|
261
|
+
"then": { "properties": { "fromNodeIds": { "maxItems": 1 } } }
|
|
262
|
+
}
|
|
263
|
+
],
|
|
258
264
|
"properties": {
|
|
259
265
|
"fromNodeIds": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" } },
|
|
266
|
+
"fallbackFromNodeIds": {
|
|
267
|
+
"type": "array",
|
|
268
|
+
"minItems": 1,
|
|
269
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
270
|
+
"description": "Optional effective-source fallbacks tried in order only when the single primary node output is absent. Each source must also appear in depends_on."
|
|
271
|
+
},
|
|
260
272
|
"requiredIds": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$" } },
|
|
261
273
|
"label": { "type": "string", "minLength": 1 }
|
|
262
274
|
}
|
|
263
275
|
},
|
|
276
|
+
"jsonArtifactGate": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"additionalProperties": false,
|
|
279
|
+
"required": ["fromNodeId", "schemaId", "artifactName", "outputDir"],
|
|
280
|
+
"properties": {
|
|
281
|
+
"fromNodeId": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
282
|
+
"fallbackFromNodeIds": {
|
|
283
|
+
"type": "array",
|
|
284
|
+
"minItems": 1,
|
|
285
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
286
|
+
"description": "Optional effective-source fallbacks tried in order only when the primary current-run node output is absent. Existing malformed or schema-invalid primary output fails closed. Each source must also appear in depends_on."
|
|
287
|
+
},
|
|
288
|
+
"schemaId": {
|
|
289
|
+
"enum": [
|
|
290
|
+
"backend-test-analysis-v1",
|
|
291
|
+
"backend-test-analysis-v2",
|
|
292
|
+
"backend-test-execution-v1",
|
|
293
|
+
"backend-test-result-v1",
|
|
294
|
+
"backend-test-classification-v1",
|
|
295
|
+
"backend-test-semantic-review-v1",
|
|
296
|
+
"backend-test-case-manifest-v1",
|
|
297
|
+
"frontend-implementation-contract-v1",
|
|
298
|
+
"frontend-test-result-v1"
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
"artifactName": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*\\.json$" },
|
|
302
|
+
"outputDir": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" },
|
|
303
|
+
"junitRelativePath": { "type": "string", "minLength": 1 }
|
|
304
|
+
}
|
|
305
|
+
},
|
|
264
306
|
"backendTestPipeline": {
|
|
265
307
|
"enum": ["contracts", "semantic-initial", "execute-parse-initial", "classification-result-context"]
|
|
266
308
|
},
|
|
@@ -281,6 +323,7 @@
|
|
|
281
323
|
{ "required": ["preset"] },
|
|
282
324
|
{ "required": ["verdictGate"] },
|
|
283
325
|
{ "required": ["requirementCoverageGate"] },
|
|
326
|
+
{ "required": ["jsonArtifactGate"] },
|
|
284
327
|
{ "required": ["backendTestPipeline"] }
|
|
285
328
|
]
|
|
286
329
|
},
|
|
@@ -319,10 +362,10 @@
|
|
|
319
362
|
"retryCategories": {
|
|
320
363
|
"type": "array",
|
|
321
364
|
"items": {
|
|
322
|
-
"enum": ["timeout", "network", "rate-limit", "unavailable"]
|
|
365
|
+
"enum": ["timeout", "network", "rate-limit", "unavailable", "output-too-large"]
|
|
323
366
|
},
|
|
324
367
|
"default": ["timeout", "network", "rate-limit", "unavailable"],
|
|
325
|
-
"description": "Failure categories eligible for retry. quota is never eligible."
|
|
368
|
+
"description": "Failure categories eligible for retry. quota is never eligible. output-too-large is reserved for explicit structured-required nodes and is not part of the default retry set."
|
|
326
369
|
}
|
|
327
370
|
}
|
|
328
371
|
},
|
|
@@ -404,6 +447,10 @@
|
|
|
404
447
|
"type": "string",
|
|
405
448
|
"minLength": 1
|
|
406
449
|
},
|
|
450
|
+
"outputMode": {
|
|
451
|
+
"enum": ["default", "structured-required"],
|
|
452
|
+
"description": "Optional output semantics for Pi nodes. structured-required means oversized assistant output must not be accepted as a truncated success; eligible read-only nodes may compact-retry and downstream gates should rely on canonical structured artifacts."
|
|
453
|
+
},
|
|
407
454
|
"firstProtocolLine": {
|
|
408
455
|
"type": "string",
|
|
409
456
|
"minLength": 1,
|
|
@@ -71,5 +71,5 @@ Do **not** invent pass rates or failure lists from raw logs when Result v1 is pr
|
|
|
71
71
|
### Non-goals
|
|
72
72
|
|
|
73
73
|
- Do not rewrite Result v1.
|
|
74
|
-
- Do not decide
|
|
74
|
+
- Do not decide pipeline completion or L-5 readiness; classification is interpretive evidence only.
|
|
75
75
|
- Do not implement M3 case manifest / Task Pool auto follow-up.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Use this prompt for a **pytest code generation** node: `executor: "pi"`, `role: "implementer"`, `toolProfile: "write"`, `writePolicy: "exclusive"`. The implementer converts
|
|
5
|
+
Use this prompt for a **pytest code generation** node: `executor: "pi"`, `role: "implementer"`, `toolProfile: "write"`, `writePolicy: "exclusive"`. The implementer converts validated backend functional test cases into pytest automation code with 1:1 traceability. The independent case review is advisory evidence and does not authorize this writer.
|
|
6
6
|
|
|
7
7
|
Do **not** create a new executor type. This is a standard `executor: pi` writer node.
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ Do **not** create a new executor type. This is a standard `executor: pi` writer
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
13
13
|
"id": "generate-backend-pytest-pi",
|
|
14
|
-
"depends_on": ["
|
|
14
|
+
"depends_on": ["backend-test-case-manifest-shell", "validate-backend-test-contracts-shell"],
|
|
15
15
|
"complexity": "HIGH",
|
|
16
16
|
"executor": "pi",
|
|
17
17
|
"role": "implementer",
|
|
@@ -38,12 +38,12 @@ Your job is to convert reviewed test cases under `testcase/md/` into pytest auto
|
|
|
38
38
|
|
|
39
39
|
### Inputs
|
|
40
40
|
|
|
41
|
-
1. **
|
|
42
|
-
2. **Validated Backend Test Analysis
|
|
41
|
+
1. **Validated test cases and manifest** — files under `testcase/md/` plus run-owned `contracts/backend-test-case-manifest.json`. The case review runs independently as advisory evidence.
|
|
42
|
+
2. **Validated Backend Test Analysis v2** — run-owned `contracts/backend-test-analysis.json`.
|
|
43
43
|
3. **Validated Backend Test Execution Contract v1** — run-owned `contracts/backend-test-execution.json` from `backend-test-execution-contract-shell` (fixtures, env *names*, `testRoot`, `targetMode`, authenticationMode).
|
|
44
44
|
4. **Target project conventions** — read `conftest.py`, `pytest.ini` / `pyproject.toml` to understand conventions, but do NOT modify them.
|
|
45
45
|
|
|
46
|
-
Do NOT re-read source documents for free-form analysis. Use only
|
|
46
|
+
Do NOT re-read source documents for free-form analysis. Use only validated cases, manifest, and contracts. Use only fixture/env/testRoot facts already present in the execution contract; never invent production credentials or secret values.
|
|
47
47
|
|
|
48
48
|
When `targetMode` is `in-process` (including demoted local npm/node managed servers): bootstrap the service inside function-scoped pytest fixtures under `testcase/**` (for example subprocess `node server.js` / `startWelcomeServer` with `PORT=0`). Never require host-injected base URL env vars such as `WELCOME_BASE_URL` / `API_BASE_URL` — the clean-env pytest shell will not provide them.
|
|
49
49
|
|