@tea-agent/loop-agent 0.16.0 → 0.16.1-beta.1
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 +1 -1
- package/CHANGELOG.md +7 -51
- package/README.md +1 -1
- package/dist/application/evaluation/candidate.js +6 -23
- package/dist/application/evaluation/types.js +0 -522
- package/dist/cli/command-definitions.js +3 -19
- package/dist/commands/eval.js +13 -1176
- package/dist/commands/init.js +1 -4
- package/dist/executors/dag-pi-executor.js +44 -4
- package/dist/worker/delivery/verification-bundle.js +2 -13
- package/dist/worker/feature/review.js +2 -3
- package/dist/worker/outcomes/adapters.js +1 -4
- package/dist/worker/outcomes/gate.js +11 -10
- package/dist/worker/outcomes/projector.js +1 -10
- package/dist/worker/outcomes/types.js +0 -3
- package/dist/worker/run-task/run-task.js +4 -23
- package/dist/worker/task-graph/ready-planner.js +8 -14
- package/dist/worker/task-graph/task-graph-schema.js +3 -5
- package/dist/workflows/dag/frontend-implementation-contract.js +77 -0
- package/dist/workflows/dag/init-hybrid.js +47 -3
- package/dist/workflows/dag/knowledge-curator.js +0 -3
- package/dist/workflows/dag/node-execution.js +4 -11
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/runner.js +16 -43
- package/dist/workflows/dag/skill-snapshot.js +7 -11
- package/dist/workflows/dag/types.js +1 -18
- package/docs/init-surface.manifest.json +0 -3
- package/docs/templates/agent-dag.schema.json +5 -0
- package/docs/templates/product-line/AGENTS.md +0 -1
- package/docs/templates/product-line/README.md +0 -17
- package/docs/templates/product-line/acceptance.yaml +0 -9
- package/docs/templates/product-line/task-graph.yaml +0 -8
- package/docs/templates/product-line/task.yaml +0 -4
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/application/evaluation/alias.js +0 -184
- package/dist/application/evaluation/budget.js +0 -192
- package/dist/application/evaluation/campaign-hash.js +0 -47
- package/dist/application/evaluation/campaign-matrix.js +0 -372
- package/dist/application/evaluation/campaign-scorecard.js +0 -135
- package/dist/application/evaluation/campaign.js +0 -370
- package/dist/application/evaluation/corpus-hash.js +0 -38
- package/dist/application/evaluation/corpus.js +0 -56
- package/dist/application/evaluation/experiment.js +0 -294
- package/dist/application/evaluation/ignition.js +0 -198
- package/dist/application/evaluation/integrity-audit.js +0 -162
- package/dist/application/evaluation/outer-loop.js +0 -132
- package/dist/application/evaluation/pi-cell-executor.js +0 -39
- package/dist/application/evaluation/private-verifier.js +0 -46
- package/dist/application/evaluation/promotion-policy.js +0 -151
- package/dist/application/evaluation/proposer.js +0 -98
- package/dist/infrastructure/evaluation/alias-store.js +0 -199
- package/dist/infrastructure/evaluation/campaign-store.js +0 -154
- package/dist/infrastructure/evaluation/corpus-store.js +0 -181
- package/dist/infrastructure/evaluation/experiment-store.js +0 -124
- package/dist/infrastructure/evaluation/ignition-store.js +0 -82
- package/dist/infrastructure/evaluation/private-verifier-store.js +0 -145
- package/dist/infrastructure/evaluation/proposer-store.js +0 -78
- package/dist/worker/outcomes/evidence-tokens.js +0 -29
- package/dist/workflows/dag/budget-enforcement.js +0 -67
- package/dist/workflows/dag/context-policy.js +0 -137
- package/docs/templates/evaluation/campaign-budget-v1.json +0 -12
- package/docs/templates/evaluation/campaign-dogfood-v0.json +0 -24
- package/docs/templates/evaluation/campaign-evidence-v1.json +0 -44
- package/docs/templates/evaluation/context-policy-baseline-v1.json +0 -17
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +0 -28
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +0 -118
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +0 -21
- package/docs/templates/evaluation/matrix-fixture-v1.json +0 -10
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +0 -16
- package/docs/templates/product-line/feature.yaml +0 -11
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../application/evaluation/budget.js";
|
|
5
4
|
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
6
5
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
7
|
-
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
8
6
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
9
7
|
import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
10
8
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
@@ -122,7 +120,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
122
120
|
: {}),
|
|
123
121
|
};
|
|
124
122
|
}
|
|
125
|
-
|
|
123
|
+
return {
|
|
126
124
|
version: 1,
|
|
127
125
|
title: spec.title,
|
|
128
126
|
runId,
|
|
@@ -149,8 +147,6 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
149
147
|
}
|
|
150
148
|
: {}),
|
|
151
149
|
};
|
|
152
|
-
initRunBudgetLedger(state, spec.budget);
|
|
153
|
-
return state;
|
|
154
150
|
}
|
|
155
151
|
export function assertFrozenEvaluationBinding(spec, state) {
|
|
156
152
|
const declared = spec.evaluation;
|
|
@@ -187,7 +183,7 @@ export async function runDag(spec, opts) {
|
|
|
187
183
|
controllerVersion: runningIdentity.packageVersion,
|
|
188
184
|
});
|
|
189
185
|
const { ranks } = topoSortToRanks(spec);
|
|
190
|
-
const
|
|
186
|
+
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
191
187
|
let runId = opts.runId;
|
|
192
188
|
if (runId) {
|
|
193
189
|
for (const warning of validateDagRunIdFormat(runId).warnings) {
|
|
@@ -272,7 +268,6 @@ export async function resumeDagRun(opts) {
|
|
|
272
268
|
throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
|
|
273
269
|
}
|
|
274
270
|
assertFrozenEvaluationBinding(spec, state);
|
|
275
|
-
assertFrozenBudget(spec.budget, state.budget, state.runId);
|
|
276
271
|
// Runtime contract + controller identity must be re-verified before executing
|
|
277
272
|
// any remaining node on resume; drift fails closed.
|
|
278
273
|
const runningIdentity = resolveRunningControllerIdentity();
|
|
@@ -316,7 +311,7 @@ export async function resumeDagRun(opts) {
|
|
|
316
311
|
catch (error) {
|
|
317
312
|
throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
313
|
}
|
|
319
|
-
const
|
|
314
|
+
const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
|
|
320
315
|
state.status = "running";
|
|
321
316
|
const resumedAt = new Date().toISOString();
|
|
322
317
|
state.runner = {
|
|
@@ -415,43 +410,23 @@ async function executeDagCheckpoint(input) {
|
|
|
415
410
|
tasksById,
|
|
416
411
|
meta: { runDir, runId: state.runId, spec },
|
|
417
412
|
}),
|
|
418
|
-
executeScheduledNode:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
spec,
|
|
432
|
-
cwd,
|
|
433
|
-
runDir,
|
|
434
|
-
executeNode,
|
|
435
|
-
executeDynamicNode,
|
|
436
|
-
observer: input.observer,
|
|
437
|
-
persistState,
|
|
438
|
-
onPause,
|
|
439
|
-
});
|
|
440
|
-
const node = state.nodes[nodeId];
|
|
441
|
-
if (node &&
|
|
442
|
-
(node.status === "FINISHED" || node.status === "ERROR")) {
|
|
443
|
-
recordFinishedNodeBudget(state, node);
|
|
444
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
445
|
-
await persistState();
|
|
446
|
-
}
|
|
447
|
-
},
|
|
413
|
+
executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
|
|
414
|
+
nodeId,
|
|
415
|
+
tasksById,
|
|
416
|
+
state,
|
|
417
|
+
spec,
|
|
418
|
+
cwd,
|
|
419
|
+
runDir,
|
|
420
|
+
executeNode,
|
|
421
|
+
executeDynamicNode,
|
|
422
|
+
observer: input.observer,
|
|
423
|
+
persistState,
|
|
424
|
+
onPause,
|
|
425
|
+
}),
|
|
448
426
|
});
|
|
449
427
|
if (pausedByNodeId) {
|
|
450
428
|
break;
|
|
451
429
|
}
|
|
452
|
-
if (isHardBudgetBreached(state.budgetLedger)) {
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
430
|
const convergenceDecision = await runConvergencePassController({
|
|
456
431
|
spec,
|
|
457
432
|
state,
|
|
@@ -471,7 +446,6 @@ async function executeDagCheckpoint(input) {
|
|
|
471
446
|
}
|
|
472
447
|
state.finishedAt = new Date().toISOString();
|
|
473
448
|
const runDirBeforeTransfer = runDir;
|
|
474
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
475
449
|
if (pausedByNodeId) {
|
|
476
450
|
state.status = "paused";
|
|
477
451
|
await persistState();
|
|
@@ -492,7 +466,6 @@ async function executeDagCheckpoint(input) {
|
|
|
492
466
|
if (state.convergence) {
|
|
493
467
|
relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
|
|
494
468
|
}
|
|
495
|
-
await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
|
|
496
469
|
await persistState({ allowCompletedFactsWrite: true });
|
|
497
470
|
return {
|
|
498
471
|
title: spec.title,
|
|
@@ -3,9 +3,9 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
|
-
import { resolveContextPolicy } from "./context-policy.js";
|
|
7
6
|
import { buildDagNodePromptEnvelope } from "./prompt.js";
|
|
8
|
-
import { resolveDagSkillInstructions, } from "./skill-instructions.js";
|
|
7
|
+
import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, resolveDagSkillInstructions, } from "./skill-instructions.js";
|
|
8
|
+
import { resolveDagNodeSkills } from "./skills.js";
|
|
9
9
|
export const SKILL_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
10
10
|
export const SKILL_SNAPSHOT_RESOLVER_VERSION = 1;
|
|
11
11
|
export const SKILL_SNAPSHOT_REL_PATH = ".runtime/skill-snapshot.json";
|
|
@@ -117,13 +117,11 @@ function profileId(input) {
|
|
|
117
117
|
export function buildDagSkillSnapshotProfileRequest(spec, task) {
|
|
118
118
|
if (task.executor !== "pi")
|
|
119
119
|
return undefined;
|
|
120
|
-
const policy = resolveContextPolicy(spec);
|
|
121
|
-
const budget = policy.resolveSkillInstructionBudget(task);
|
|
122
120
|
const request = {
|
|
123
|
-
skills: [...
|
|
124
|
-
includeLearnedPatterns:
|
|
125
|
-
perSkillMaxChars:
|
|
126
|
-
totalMaxChars:
|
|
121
|
+
skills: [...resolveDagNodeSkills(spec, task)],
|
|
122
|
+
includeLearnedPatterns: task.role === "implementer",
|
|
123
|
+
perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
|
|
124
|
+
totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
|
|
127
125
|
};
|
|
128
126
|
return { id: profileId(request), ...request };
|
|
129
127
|
}
|
|
@@ -513,8 +511,7 @@ function stripPromptText(instruction) {
|
|
|
513
511
|
return metadata;
|
|
514
512
|
}
|
|
515
513
|
export function buildNodePromptFromSnapshot(input) {
|
|
516
|
-
const
|
|
517
|
-
const skillNames = policy.resolveSkills(input.spec, input.task);
|
|
514
|
+
const skillNames = resolveDagNodeSkills(input.spec, input.task);
|
|
518
515
|
const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
|
|
519
516
|
return {
|
|
520
517
|
prompt: buildDagNodePromptEnvelope({
|
|
@@ -523,7 +520,6 @@ export function buildNodePromptFromSnapshot(input) {
|
|
|
523
520
|
upstream: input.upstream,
|
|
524
521
|
resolvedSkills: skillNames,
|
|
525
522
|
resolvedSkillInstructions,
|
|
526
|
-
maxUpstreamChars: policy.resolveMaxUpstreamChars(input.task),
|
|
527
523
|
}),
|
|
528
524
|
resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
|
|
529
525
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { campaignBudgetSchema, } from "../../application/evaluation/budget.js";
|
|
3
2
|
import { assertDagPromptSourceRule } from "./prompt-source.js";
|
|
4
3
|
import { dagRetryPolicySchema } from "./retry-policy.js";
|
|
5
4
|
export const dagComplexitySchema = z.enum(["HIGH", "MED", "LOW"]);
|
|
@@ -106,18 +105,12 @@ export const dagWritePolicySchema = z.enum([
|
|
|
106
105
|
"exclusive",
|
|
107
106
|
"none",
|
|
108
107
|
]);
|
|
109
|
-
export const contextPolicyIdSchema = z.enum([
|
|
110
|
-
"baseline-v1",
|
|
111
|
-
"role-specialized-v1",
|
|
112
|
-
]);
|
|
113
108
|
export const dagDefaultsSchema = z
|
|
114
109
|
.object({
|
|
115
110
|
executor: dagNodeExecutorSchema.optional(),
|
|
116
111
|
model: z.string().optional(),
|
|
117
112
|
piBackend: z.enum(["sdk-first", "cli-only"]).optional(),
|
|
118
113
|
contextProfile: z.string().optional(),
|
|
119
|
-
/** Eval Lab ContextPolicy seam; default baseline-v1 when omitted. */
|
|
120
|
-
contextPolicyId: contextPolicyIdSchema.optional(),
|
|
121
114
|
skills: z.array(z.string()).optional(),
|
|
122
115
|
writePolicy: dagWritePolicySchema.optional(),
|
|
123
116
|
})
|
|
@@ -274,6 +267,7 @@ export const dagTaskSchema = z.object({
|
|
|
274
267
|
shell: dagShellConfigSchema.optional(),
|
|
275
268
|
static: dagStaticConfigSchema.optional(),
|
|
276
269
|
outputContract: z.string().optional(),
|
|
270
|
+
firstProtocolLine: z.string().min(1).optional(),
|
|
277
271
|
allowedPaths: z.array(z.string()).optional().default([]),
|
|
278
272
|
forbiddenPaths: z.array(z.string()).optional().default([]),
|
|
279
273
|
decisionGate: dagDecisionGateSchema.optional(),
|
|
@@ -307,8 +301,6 @@ export const dagEvaluationBindingSchema = z
|
|
|
307
301
|
taskRef: z.string().min(1).optional(),
|
|
308
302
|
})
|
|
309
303
|
.strict();
|
|
310
|
-
/** Eval Lab Campaign Budget (W3.4); enforced by runner ledger when mode=hard. */
|
|
311
|
-
export const dagBudgetSchema = campaignBudgetSchema;
|
|
312
304
|
export const dagSourceBindingSchema = z.object({
|
|
313
305
|
schemaVersion: z.literal(1),
|
|
314
306
|
taskId: z.string().min(1),
|
|
@@ -329,8 +321,6 @@ export const dagSpecSchema = z
|
|
|
329
321
|
title: z.string().min(1),
|
|
330
322
|
runtimeContract: dagRuntimeContractSchema.optional(),
|
|
331
323
|
evaluation: dagEvaluationBindingSchema.optional(),
|
|
332
|
-
/** Optional hard/record-only budget; requires version 3. */
|
|
333
|
-
budget: dagBudgetSchema.optional(),
|
|
334
324
|
sourceBinding: dagSourceBindingSchema.optional(),
|
|
335
325
|
outputLanguage: dagOutputLanguageSchema.optional(),
|
|
336
326
|
objective: z.string().optional(),
|
|
@@ -351,13 +341,6 @@ export const dagSpecSchema = z
|
|
|
351
341
|
path: ["evaluation"],
|
|
352
342
|
});
|
|
353
343
|
}
|
|
354
|
-
if (spec.budget && spec.version !== 3) {
|
|
355
|
-
ctx.addIssue({
|
|
356
|
-
code: z.ZodIssueCode.custom,
|
|
357
|
-
message: "budget requires DagSpec version 3",
|
|
358
|
-
path: ["budget"],
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
344
|
if (spec.runtimeContract && spec.version !== 3) {
|
|
362
345
|
ctx.addIssue({
|
|
363
346
|
code: z.ZodIssueCode.custom,
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"docs/templates/product-line/test-plan.md",
|
|
40
40
|
"docs/templates/product-line/task-graph.yaml",
|
|
41
41
|
"docs/templates/product-line/task.yaml",
|
|
42
|
-
"docs/templates/product-line/feature.yaml",
|
|
43
42
|
"docs/templates/product-line/closeout.yaml",
|
|
44
43
|
"docs/templates/product-line/links.md",
|
|
45
44
|
"scripts/kb-bootstrap-init-skeleton.sh",
|
|
@@ -107,7 +106,6 @@
|
|
|
107
106
|
"docs/templates/product-line/test-plan.md",
|
|
108
107
|
"docs/templates/product-line/task-graph.yaml",
|
|
109
108
|
"docs/templates/product-line/task.yaml",
|
|
110
|
-
"docs/templates/product-line/feature.yaml",
|
|
111
109
|
"docs/templates/product-line/closeout.yaml",
|
|
112
110
|
"docs/templates/product-line/links.md",
|
|
113
111
|
"scripts/check-skill-entry.sh",
|
|
@@ -177,7 +175,6 @@
|
|
|
177
175
|
"docs/templates/product-line/test-plan.md": "copied",
|
|
178
176
|
"docs/templates/product-line/task-graph.yaml": "copied",
|
|
179
177
|
"docs/templates/product-line/task.yaml": "copied",
|
|
180
|
-
"docs/templates/product-line/feature.yaml": "copied",
|
|
181
178
|
"docs/templates/product-line/closeout.yaml": "copied",
|
|
182
179
|
"docs/templates/product-line/links.md": "copied",
|
|
183
180
|
"scripts/check-skill-entry.sh": "generated",
|
|
@@ -366,6 +366,11 @@
|
|
|
366
366
|
"type": "string",
|
|
367
367
|
"minLength": 1
|
|
368
368
|
},
|
|
369
|
+
"firstProtocolLine": {
|
|
370
|
+
"type": "string",
|
|
371
|
+
"minLength": 1,
|
|
372
|
+
"description": "Optional protocol prefix whose first matching Pi assistant-output line is promoted to the canonical first line. Missing matches are not synthesized."
|
|
373
|
+
},
|
|
369
374
|
"allowedPaths": {
|
|
370
375
|
"type": "array",
|
|
371
376
|
"items": { "type": "string", "minLength": 1 },
|
|
@@ -4,6 +4,5 @@
|
|
|
4
4
|
- Treat `constraints.allowed_paths` and `constraints.forbidden_paths` as machine-enforced write boundaries; review DAG writer `writeSet` before execution.
|
|
5
5
|
- Advance only tasks whose dependencies are complete. Preserve failed run records; retries receive new worker run IDs.
|
|
6
6
|
- A Ready task must have acceptance references, non-empty allowed/forbidden paths, and deterministic verification commands.
|
|
7
|
-
- For `fullstack-v1` packets (`feature.yaml`), every TaskSpec should declare `execution.workflow`; required ACs need implementation/verification refs, `required_evidence`, and integration policy. Parallel writers must not share overlapping `allowed_paths`.
|
|
8
7
|
- QA records an independent verdict and evidence. Do not write `status: success` closeout until `qa_verdict: pass` and non-empty `qa_evidence` exist.
|
|
9
8
|
- Human gates remain human decisions. Record owner, time, reason, evidence, and follow-up without rewriting failed history.
|
|
@@ -7,20 +7,3 @@ agent-worker task validate-feature <feature-dir>
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
The validator checks acceptance ID uniqueness, task references and dependencies, cycles, TaskSpec/path/verification completeness, and QA evidence before a successful closeout.
|
|
10
|
-
|
|
11
|
-
## Profiles
|
|
12
|
-
|
|
13
|
-
| Profile | How to declare | Behavior |
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| `generic`(默认) | 省略 `feature.yaml`,或 `profile: generic` | 保持 legacy 校验;不强制 `execution.workflow` / 双覆盖 AC |
|
|
16
|
-
| `fullstack-v1` | `feature.yaml` 中 `profile: fullstack-v1` 并声明 `scope` | 强制显式 workflow、required AC 双覆盖、frontend-test 依赖、writer writeSet 串行等结构门禁 |
|
|
17
|
-
|
|
18
|
-
端到端全栈 dogfood 样板见仓库 `features/F-2026-005/`(欢迎语链路:契约 → BE/FE 实现 → backend-test / frontend-test → Final Verification)。
|
|
19
|
-
|
|
20
|
-
## Minimal files
|
|
21
|
-
|
|
22
|
-
- `feature.yaml`(可选;fullstack 必填)
|
|
23
|
-
- `requirement.md` / `design.md` / `test-plan.md`
|
|
24
|
-
- `acceptance.yaml`
|
|
25
|
-
- `tasks/task-graph.yaml` + `tasks/*.yaml`
|
|
26
|
-
- `links.md` / `closeout.yaml`(按交付阶段)
|
|
@@ -12,12 +12,3 @@ acceptance:
|
|
|
12
12
|
expected_task_refs:
|
|
13
13
|
- BE-001
|
|
14
14
|
- QA-001
|
|
15
|
-
# fullstack-v1 required AC fields (optional for generic packets):
|
|
16
|
-
# implementation_task_refs:
|
|
17
|
-
# - BE-001
|
|
18
|
-
# verification_task_refs:
|
|
19
|
-
# - BE-TEST-001
|
|
20
|
-
# required_evidence:
|
|
21
|
-
# - backend-test-result-v1
|
|
22
|
-
# - shell_verification
|
|
23
|
-
# integration: real-required
|
|
@@ -5,19 +5,11 @@ nodes:
|
|
|
5
5
|
task: BE-001.yaml
|
|
6
6
|
type: backend-feature
|
|
7
7
|
depends_on: []
|
|
8
|
-
# Optional artifact-aware edges (Ready Planner envelope-level gate):
|
|
9
|
-
# produces:
|
|
10
|
-
# - kind: backend-impl
|
|
11
|
-
# schemaId: backend-impl-v1
|
|
12
8
|
- id: QA-001
|
|
13
9
|
task: QA-001.yaml
|
|
14
10
|
type: qa-execute
|
|
15
11
|
depends_on:
|
|
16
12
|
- BE-001
|
|
17
|
-
# consumes:
|
|
18
|
-
# - kind: backend-impl
|
|
19
|
-
# schemaId: backend-impl-v1
|
|
20
|
-
# producerTaskId: BE-001
|
|
21
13
|
parallel_policy:
|
|
22
14
|
max_parallel_tasks: 2
|
|
23
15
|
disallow_same_file_parallel_writes: true
|
|
@@ -30,10 +30,6 @@ constraints:
|
|
|
30
30
|
- <protected/path/**>
|
|
31
31
|
hard_constraints:
|
|
32
32
|
- Do not access production secrets
|
|
33
|
-
# Explicit runtime workflow → taskKind. For fullstack-v1 packets this should be set
|
|
34
|
-
# on every TaskSpec. Allowed: agent-dag | frontend-implementation | backend-test | frontend-test
|
|
35
|
-
execution:
|
|
36
|
-
workflow: agent-dag
|
|
37
33
|
verify:
|
|
38
34
|
preset: standard
|
|
39
35
|
mode: serial
|
package/package.json
CHANGED
|
@@ -6,10 +6,10 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
|
|
|
6
6
|
|
|
7
7
|
- **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`. No guessed requirements.
|
|
8
8
|
- **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Knowledge base first; else search+read `<repoRoot>/openSpec/**` before repo fallback. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
|
|
9
|
-
- **`frontend-mock-assess-pi` + gate**: first
|
|
9
|
+
- **`frontend-mock-assess-pi` + gate**: declares `firstProtocolLine: "MOCK_STRATEGY:"`; canonical output first line
|
|
10
10
|
`MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked`
|
|
11
|
-
Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
|
|
12
|
-
- **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix).
|
|
11
|
+
Pi output mapping promotes the first matching protocol line ahead of any preamble without inventing or replacing its value; missing, malformed, or blocked strategies still fail closed. Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
|
|
12
|
+
- **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix). The plan-pi prompt now includes the complete `frontend-implementation-contract-v1` JSON Schema loaded from the loop-agent package `docs/templates/` path, plus deterministic source binding, risk level, and allowed implementation targets. The model does not need to search or guess contract fields; `schemaId`, `targetFiles`, `requirementCoverage` are explicitly forbidden.
|
|
13
13
|
- **`frontend-implement-pi`**: sole exclusive writer. Stay in `writeSet`; real requests default-on; Mock reversible, dev/test-only, production-off. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths or guesses. Output changed files, behavior, UI states, styling notes, verification attempted, residual risks. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. Skipped-Mock `not-needed` keeps real integration pending unless the real backend path has fresh evidence.
|
|
14
14
|
|
|
15
15
|
## Contract / trace / stages (M1–M2)
|
|
@@ -117,7 +117,7 @@ contract-pi → scout-src ∥ scout-tests → plan-pi → write-set-audit-pi
|
|
|
117
117
|
| `authority-surface-audit-pi` + `authority-surface-gate-shell` | 可选 permission/state/tool-exposure audit;仅 authority signal 或显式 `authority-surface-audit` marker 时插入;gate 仅接受 `VERDICT: pass` |
|
|
118
118
|
| `review-pi` + `review-gate-shell` | Critical/Important → `request-revision`;node JSON 上 `shell.verdictGate` block,除非 extracted verdict 行为 `VERDICT: pass` |
|
|
119
119
|
|
|
120
|
-
**Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty`
|
|
120
|
+
**Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty` 以兼容;需要固定非 `VERDICT:` 协议首行的 Pi 节点可显式声明 `firstProtocolLine`,executor 会把第一条匹配前缀的行提升为 canonical 首行,不匹配时不伪造。`frontend-mock-assess-pi` 用它固定 `MOCK_STRATEGY:`,gate 仍保持 `first-non-empty` exact-match。supervised gate 用 `first-verdict-line` 选 Pi 在 preamble 或常见整行 Markdown emphasis(如 `**VERDICT: pass**`)后第一条 normalized `VERDICT:` line。勿用 `result.summary.md`、grep VERDICT、latest-active-run discovery 或 multi-command stateful gate。`--strict-governance` 对 anti-pattern fail。supervisor 仍为 `executor: pi` 上的 `role: supervisor`。
|
|
121
121
|
|
|
122
122
|
**Repair artifact gate contract(`shell.repairArtifactGate`)**:声明 `fromNodeId`(supervisor artifact 节点)与 `repairNodeId`(承接修订的 Pi 修复节点)。runner **不再**按节点名(历史 `repair-cursor` / `repair-pi`)猜测 repair 节点:显式 `repairNodeId` 必须存在、直接 `depends_on` gate、且是受治理 Pi writer(`executor: pi`、`toolProfile: write`、`writePolicy: exclusive`、`allowedPaths`+`writeSet` 非空且 `writeSet` 不与 `forbiddenPaths` 冲突)。新生成的 supervised DAG 总是写入 `repairNodeId`;旧 DAG 缺失时只在能唯一、安全地推导出下游 Pi writer 时兼容,零个或多个候选、或候选不满足契约都在执行前 fail closed。validation 覆盖存在性、直接下游、writer 属性与路径边界。
|
|
123
123
|
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { aliasHistoryPath, commitAliasMove, listAliasNames, listIncumbentCandidateIds, makeDecisionId, readIncumbentAlias, readPromotionDecision, } from "../../infrastructure/evaluation/alias-store.js";
|
|
3
|
-
import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
|
|
4
|
-
async function requireAcceptedCandidate(input) {
|
|
5
|
-
const record = await readCandidateRecord(input.repoRoot, input.candidateId);
|
|
6
|
-
if (record.status !== "accepted") {
|
|
7
|
-
throw new Error(`alias move requires candidate lifecycle accepted; ${input.candidateId} is ${record.status}`);
|
|
8
|
-
}
|
|
9
|
-
return {
|
|
10
|
-
candidateId: record.manifest.candidateId,
|
|
11
|
-
bundleHash: record.manifest.bundleHash,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function buildDecision(input) {
|
|
15
|
-
const decisionId = makeDecisionId({
|
|
16
|
-
alias: input.alias,
|
|
17
|
-
action: input.action,
|
|
18
|
-
toCandidateId: input.toCandidateId,
|
|
19
|
-
createdAt: input.createdAt,
|
|
20
|
-
});
|
|
21
|
-
return {
|
|
22
|
-
schemaVersion: 1,
|
|
23
|
-
decisionId,
|
|
24
|
-
alias: input.alias,
|
|
25
|
-
action: input.action,
|
|
26
|
-
fromCandidateId: input.fromCandidateId,
|
|
27
|
-
toCandidateId: input.toCandidateId,
|
|
28
|
-
toBundleHash: input.toBundleHash,
|
|
29
|
-
reason: input.reason,
|
|
30
|
-
actor: "human",
|
|
31
|
-
humanRequired: true,
|
|
32
|
-
applied: input.applied,
|
|
33
|
-
createdAt: input.createdAt,
|
|
34
|
-
campaignId: input.campaignId ?? null,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
async function planAliasMove(input) {
|
|
38
|
-
const reason = input.reason.trim();
|
|
39
|
-
if (!reason) {
|
|
40
|
-
throw new Error("alias move requires non-empty --reason");
|
|
41
|
-
}
|
|
42
|
-
const createdAt = input.now ?? new Date().toISOString();
|
|
43
|
-
const previous = await readIncumbentAlias(input.repoRoot, input.alias);
|
|
44
|
-
if (input.action === "rollback" && !previous) {
|
|
45
|
-
throw new Error(`cannot rollback alias ${input.alias}: alias does not exist yet`);
|
|
46
|
-
}
|
|
47
|
-
const target = await requireAcceptedCandidate({
|
|
48
|
-
repoRoot: input.repoRoot,
|
|
49
|
-
candidateId: input.toCandidateId,
|
|
50
|
-
});
|
|
51
|
-
if (previous && previous.candidateId === target.candidateId) {
|
|
52
|
-
throw new Error(`alias ${input.alias} already points at candidate ${target.candidateId}`);
|
|
53
|
-
}
|
|
54
|
-
const decision = buildDecision({
|
|
55
|
-
alias: input.alias,
|
|
56
|
-
action: input.action,
|
|
57
|
-
fromCandidateId: previous?.candidateId ?? null,
|
|
58
|
-
toCandidateId: target.candidateId,
|
|
59
|
-
toBundleHash: target.bundleHash,
|
|
60
|
-
reason,
|
|
61
|
-
applied: !input.dryRun,
|
|
62
|
-
createdAt,
|
|
63
|
-
campaignId: input.campaignId,
|
|
64
|
-
});
|
|
65
|
-
const nextAlias = {
|
|
66
|
-
schemaVersion: 1,
|
|
67
|
-
alias: input.alias,
|
|
68
|
-
candidateId: target.candidateId,
|
|
69
|
-
bundleHash: target.bundleHash,
|
|
70
|
-
updatedAt: createdAt,
|
|
71
|
-
updatedByDecisionId: decision.decisionId,
|
|
72
|
-
previousCandidateId: previous?.candidateId ?? null,
|
|
73
|
-
};
|
|
74
|
-
if (input.dryRun) {
|
|
75
|
-
return {
|
|
76
|
-
dryRun: true,
|
|
77
|
-
decision: { ...decision, applied: false },
|
|
78
|
-
alias: nextAlias,
|
|
79
|
-
previous,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const historySeq = previous ? await nextHistorySeq(input.repoRoot, input.alias) : 1;
|
|
83
|
-
const historyEvent = {
|
|
84
|
-
schemaVersion: 1,
|
|
85
|
-
seq: historySeq,
|
|
86
|
-
decisionId: decision.decisionId,
|
|
87
|
-
action: input.action,
|
|
88
|
-
fromCandidateId: previous?.candidateId ?? null,
|
|
89
|
-
toCandidateId: target.candidateId,
|
|
90
|
-
toBundleHash: target.bundleHash,
|
|
91
|
-
at: createdAt,
|
|
92
|
-
};
|
|
93
|
-
await commitAliasMove({
|
|
94
|
-
repoRoot: input.repoRoot,
|
|
95
|
-
decision,
|
|
96
|
-
nextAlias,
|
|
97
|
-
historyEvent,
|
|
98
|
-
});
|
|
99
|
-
// W2.4 does not auto-retire the previous incumbent: lifecycle `accepted` ≠
|
|
100
|
-
// alias pointer, and rollback drills must be able to point back without
|
|
101
|
-
// inventing a new candidate id. Operators may `eval candidate transition
|
|
102
|
-
// --to retired` separately.
|
|
103
|
-
return {
|
|
104
|
-
dryRun: false,
|
|
105
|
-
decision,
|
|
106
|
-
alias: (await readIncumbentAlias(input.repoRoot, input.alias)),
|
|
107
|
-
previous,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
async function nextHistorySeq(repoRoot, alias) {
|
|
111
|
-
const text = await readFile(aliasHistoryPath(repoRoot, alias), "utf-8").catch(() => "");
|
|
112
|
-
const lines = text
|
|
113
|
-
.split("\n")
|
|
114
|
-
.map((line) => line.trim())
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
return lines.length + 1;
|
|
117
|
-
}
|
|
118
|
-
export async function promoteAlias(input) {
|
|
119
|
-
return planAliasMove({
|
|
120
|
-
...input,
|
|
121
|
-
action: "promote",
|
|
122
|
-
dryRun: input.dryRun !== false,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
export async function rollbackAlias(input) {
|
|
126
|
-
return planAliasMove({
|
|
127
|
-
...input,
|
|
128
|
-
action: "rollback",
|
|
129
|
-
dryRun: input.dryRun !== false,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
export async function showAlias(input) {
|
|
133
|
-
const current = await readIncumbentAlias(input.repoRoot, input.alias);
|
|
134
|
-
if (!current) {
|
|
135
|
-
throw new Error(`alias not found: ${input.alias}`);
|
|
136
|
-
}
|
|
137
|
-
return current;
|
|
138
|
-
}
|
|
139
|
-
export async function listAliases(input) {
|
|
140
|
-
const names = await listAliasNames(input.repoRoot);
|
|
141
|
-
const rows = [];
|
|
142
|
-
for (const name of names) {
|
|
143
|
-
const current = await readIncumbentAlias(input.repoRoot, name);
|
|
144
|
-
if (current)
|
|
145
|
-
rows.push(current);
|
|
146
|
-
}
|
|
147
|
-
return rows;
|
|
148
|
-
}
|
|
149
|
-
export async function isCandidatePromotionApplied(input) {
|
|
150
|
-
const incumbents = await listIncumbentCandidateIds(input.repoRoot);
|
|
151
|
-
return incumbents.has(input.candidateId);
|
|
152
|
-
}
|
|
153
|
-
export function formatAliasMarkdown(alias) {
|
|
154
|
-
return [
|
|
155
|
-
`# Alias: ${alias.alias}`,
|
|
156
|
-
"",
|
|
157
|
-
`- candidateId: \`${alias.candidateId}\``,
|
|
158
|
-
`- bundleHash: \`${alias.bundleHash}\``,
|
|
159
|
-
`- previousCandidateId: \`${alias.previousCandidateId ?? "null"}\``,
|
|
160
|
-
`- updatedByDecisionId: \`${alias.updatedByDecisionId}\``,
|
|
161
|
-
`- updatedAt: \`${alias.updatedAt}\``,
|
|
162
|
-
"",
|
|
163
|
-
].join("\n");
|
|
164
|
-
}
|
|
165
|
-
export function formatAliasMoveMarkdown(result) {
|
|
166
|
-
const mode = result.dryRun ? "dry-run" : "applied";
|
|
167
|
-
return [
|
|
168
|
-
`# Alias ${result.decision.action} (${mode})`,
|
|
169
|
-
"",
|
|
170
|
-
`- alias: \`${result.decision.alias}\``,
|
|
171
|
-
`- from: \`${result.decision.fromCandidateId ?? "null"}\``,
|
|
172
|
-
`- to: \`${result.decision.toCandidateId}\``,
|
|
173
|
-
`- bundleHash: \`${result.decision.toBundleHash}\``,
|
|
174
|
-
`- decisionId: \`${result.decision.decisionId}\``,
|
|
175
|
-
`- applied: \`${result.decision.applied}\``,
|
|
176
|
-
`- reason: ${result.decision.reason}`,
|
|
177
|
-
"",
|
|
178
|
-
result.dryRun
|
|
179
|
-
? "_No alias files were written (dry-run). Pass `--apply` to commit._"
|
|
180
|
-
: "_Alias pointer updated; candidate bundles and completed DAG facts were not rewritten._",
|
|
181
|
-
"",
|
|
182
|
-
].join("\n");
|
|
183
|
-
}
|
|
184
|
-
export { readPromotionDecision };
|