@tea-agent/loop-agent 0.27.1 → 0.28.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 +24 -0
- package/dist/application/task-lifecycle/observe.js +5 -0
- package/dist/application/task-lifecycle/plan-transitions.js +7 -2
- package/dist/cli/program.js +1 -1
- package/dist/commands/client-recovery.js +439 -20
- package/dist/commands/init.js +42 -6
- package/dist/executors/dag-pi-executor.js +143 -38
- package/dist/executors/pi-playwright-cli-tool.js +955 -0
- package/dist/executors/pi-sdk-executor.js +56 -0
- package/dist/executors/playwright-cli-launcher.js +63 -0
- package/dist/executors/shell-executor.js +128 -0
- package/dist/shared/playwright-cli-command-policy.js +41 -0
- package/dist/worker/observability/read-model.js +66 -8
- package/dist/worker/observe/static/dag-model.js +85 -13
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +4 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +13 -13
- package/dist/workflows/dag/frontend-test-case-checklist.js +201 -8
- package/dist/workflows/dag/frontend-test-result-contract.js +52 -3
- package/dist/workflows/dag/init-hybrid.js +116 -30
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/node-execution.js +11 -5
- package/dist/workflows/dag/output-protocol.js +25 -83
- package/dist/workflows/dag/report.js +9 -2
- package/dist/workflows/dag/rerun-run.js +62 -3
- package/dist/workflows/dag/run-store.js +6 -1
- package/dist/workflows/dag/runner.js +15 -3
- package/dist/workflows/dag/types.js +27 -0
- package/dist/workflows/dag/validate.js +121 -1
- package/docs/architecture/runtime-boundaries.md +13 -11
- package/docs/init-surface.manifest.json +6 -2
- package/docs/templates/README.md +9 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +14 -7
- package/docs/templates/frontend-test-dag.json +55 -15
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +7 -9
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +1 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +18 -6
- package/skills/playwright-cli/SKILL.md +69 -402
- package/skills/playwright-cli/references/tracing.md +3 -137
- package/skills/playwright-cli/references/video-recording.md +3 -141
- package/skills/playwright-cli-case-generator/SKILL.md +53 -46
|
@@ -4,9 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
6
6
|
import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
|
|
7
|
-
import { DAG_RUNS_DIR, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
7
|
+
import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
8
8
|
import { buildDagRunId, runDagContinuation, } from "./runner.js";
|
|
9
|
-
import { evaluateDagRerunPlan, evaluateLiveDagRerunBindings, } from "./rerun-plan.js";
|
|
9
|
+
import { computePlanHash, evaluateDagRerunPlan, evaluateLiveDagRerunBindings, } from "./rerun-plan.js";
|
|
10
10
|
import { prepareActiveRunDir, writeNodeRecord, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
11
11
|
import { cloneParentSkillSnapshotForContinuation, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
12
12
|
import { topoSortToRanks } from "./topo.js";
|
|
@@ -123,6 +123,65 @@ export async function planDagRerun(input) {
|
|
|
123
123
|
const nonCompletedSpec = await readDagRunSpec(located.runDir);
|
|
124
124
|
const nonCompletedState = await readDagRunState(located.runDir);
|
|
125
125
|
const nonCompletedWorkerHints = await detectWorkerManagedRun(located.runDir, nonCompletedState, nonCompletedSpec);
|
|
126
|
+
// evaluateDagRerunPlan only accepts execution lifecycles. Audit parents must
|
|
127
|
+
// not be cast to "active" (reconcile) or "paused" (resume). Also avoid routing
|
|
128
|
+
// audit through evaluate: missing workspace checkpoint would surface as
|
|
129
|
+
// standalone-task-rerun instead of the required fail-closed manual action.
|
|
130
|
+
if (isDagNonExecutionLifecycle(located.lifecycle)) {
|
|
131
|
+
const bindingStatus = await evaluateLiveDagRerunBindings({
|
|
132
|
+
cwd: input.cwd,
|
|
133
|
+
parentSpec: nonCompletedSpec,
|
|
134
|
+
});
|
|
135
|
+
const workerAssociation = nonCompletedWorkerHints.workerManaged
|
|
136
|
+
? {
|
|
137
|
+
kind: "worker-managed",
|
|
138
|
+
...(nonCompletedWorkerHints.featureId
|
|
139
|
+
? { featureId: nonCompletedWorkerHints.featureId }
|
|
140
|
+
: {}),
|
|
141
|
+
...(nonCompletedWorkerHints.taskId
|
|
142
|
+
? { taskId: nonCompletedWorkerHints.taskId }
|
|
143
|
+
: {}),
|
|
144
|
+
...(nonCompletedWorkerHints.workerRunId
|
|
145
|
+
? { workerRunId: nonCompletedWorkerHints.workerRunId }
|
|
146
|
+
: {}),
|
|
147
|
+
}
|
|
148
|
+
: { kind: "standalone" };
|
|
149
|
+
const planWithoutHash = {
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
eligible: false,
|
|
152
|
+
parentRunId: input.parentRunId,
|
|
153
|
+
parentLifecycle: located.lifecycle,
|
|
154
|
+
selectedNodeId: input.selectedNodeId,
|
|
155
|
+
rewriteApplied: false,
|
|
156
|
+
resetNodeIds: [],
|
|
157
|
+
importedNodeIds: [],
|
|
158
|
+
blockingNodes: [],
|
|
159
|
+
reasonCodes: ["parent-lifecycle-ineligible"],
|
|
160
|
+
blockedReasons: ["parent-lifecycle-ineligible"],
|
|
161
|
+
risk: "high",
|
|
162
|
+
bindings: {
|
|
163
|
+
sourceBindingMatched: bindingStatus.sourceBindingMatched,
|
|
164
|
+
taskContractBindingMatched: bindingStatus.taskContractBindingMatched,
|
|
165
|
+
controllerIdentityMatched: true,
|
|
166
|
+
skillSnapshotVerified: true,
|
|
167
|
+
workspaceMatched: false,
|
|
168
|
+
},
|
|
169
|
+
workerAssociation,
|
|
170
|
+
estimatedExecution: {
|
|
171
|
+
nodeCount: 0,
|
|
172
|
+
piCallCount: 0,
|
|
173
|
+
shellNodeCount: 0,
|
|
174
|
+
avoidedNodeCount: 0,
|
|
175
|
+
avoidedPiCallCount: 0,
|
|
176
|
+
},
|
|
177
|
+
suggestedAction: "manual",
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
...planWithoutHash,
|
|
181
|
+
planHash: computePlanHash(planWithoutHash),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const parentLifecycle = located.lifecycle === "active" ? "active" : "paused";
|
|
126
185
|
return evaluateDagRerunPlan({
|
|
127
186
|
cwd: input.cwd,
|
|
128
187
|
parentRunId: input.parentRunId,
|
|
@@ -130,7 +189,7 @@ export async function planDagRerun(input) {
|
|
|
130
189
|
parentRunDir: located.runDir,
|
|
131
190
|
parentSpec: nonCompletedSpec,
|
|
132
191
|
parentState: nonCompletedState,
|
|
133
|
-
parentLifecycle
|
|
192
|
+
parentLifecycle,
|
|
134
193
|
workerManaged: nonCompletedWorkerHints.workerManaged,
|
|
135
194
|
workerAssociation: nonCompletedWorkerHints.workerManaged
|
|
136
195
|
? {
|
|
@@ -11,10 +11,15 @@ export async function writeRunState(runDir, state, options) {
|
|
|
11
11
|
export async function readRunSpec(runDir) {
|
|
12
12
|
return readDagRunSpec(runDir);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/** Prepare any lifecycle run directory (active / dry-run / init-only / …). */
|
|
15
|
+
export async function prepareRunDir(runDir) {
|
|
15
16
|
await rm(runDir, { recursive: true, force: true });
|
|
16
17
|
await mkdir(runDir, { recursive: true });
|
|
17
18
|
}
|
|
19
|
+
/** @deprecated Prefer prepareRunDir; kept as thin alias for execute callers. */
|
|
20
|
+
export async function prepareActiveRunDir(runDir) {
|
|
21
|
+
await prepareRunDir(runDir);
|
|
22
|
+
}
|
|
18
23
|
export async function writeRunSpec(runDir, spec) {
|
|
19
24
|
await writeJsonAtomic(path.join(runDir, "run.json"), spec);
|
|
20
25
|
}
|
|
@@ -7,7 +7,7 @@ import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-s
|
|
|
7
7
|
import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
|
|
8
8
|
import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
|
|
9
9
|
import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
|
|
10
|
-
import { moveToCompletedRunDir, moveToPausedRunDir,
|
|
10
|
+
import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
|
|
11
11
|
import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
12
12
|
import { createDagNodeExecutor } from "./executor-registry.js";
|
|
13
13
|
import { executeDagPiNode } from "../../executors/dag-pi-executor.js";
|
|
@@ -126,12 +126,18 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
|
|
|
126
126
|
: {}),
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
const executionMode = opts.dryRun
|
|
130
|
+
? "dry-run"
|
|
131
|
+
: opts.initOnly
|
|
132
|
+
? "init-only"
|
|
133
|
+
: "execute";
|
|
129
134
|
const state = {
|
|
130
135
|
version: 1,
|
|
131
136
|
title: spec.title,
|
|
132
137
|
runId,
|
|
133
138
|
cwd: opts.cwd,
|
|
134
139
|
startedAt: new Date().toISOString(),
|
|
140
|
+
executionMode,
|
|
135
141
|
runner: {
|
|
136
142
|
pid: process.pid,
|
|
137
143
|
hostname: hostname(),
|
|
@@ -212,8 +218,14 @@ export async function runDag(spec, opts) {
|
|
|
212
218
|
const activeRunDir = getDagRunDir(opts.cwd, "active", state.runId);
|
|
213
219
|
const completedRunDir = getDagRunDir(opts.cwd, "completed", state.runId);
|
|
214
220
|
const pausedRunDir = getDagRunDir(opts.cwd, "paused", state.runId);
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
// R-A2: non-execution modes use dedicated lifecycles; only execute enters active/.
|
|
222
|
+
const lifecycleDir = state.executionMode === "dry-run"
|
|
223
|
+
? "dry-run"
|
|
224
|
+
: state.executionMode === "init-only"
|
|
225
|
+
? "init-only"
|
|
226
|
+
: "active";
|
|
227
|
+
const runDir = getDagRunDir(opts.cwd, lifecycleDir, state.runId);
|
|
228
|
+
await prepareRunDir(runDir);
|
|
217
229
|
await writeRunSpec(runDir, spec);
|
|
218
230
|
// Freeze the running controller identity as a run-owned fact before the
|
|
219
231
|
// first canonical state write, reusing the identity resolved for preflight.
|
|
@@ -10,6 +10,27 @@ export const dagNodeExecutorSchema = z.enum(["pi", "shell", "static"]);
|
|
|
10
10
|
export const CURSOR_DAG_EXECUTOR_REMOVED_ERROR = 'executor "cursor" is no longer supported; regenerate the DAG with Pi-only writers (implement-pi / repair-pi)';
|
|
11
11
|
export const CURSOR_EXECUTOR_MODELS_REMOVED_ERROR = "executorModels.cursor is no longer supported; use executorModels.pi only";
|
|
12
12
|
export const dagToolProfileSchema = z.enum(["read-only", "write"]);
|
|
13
|
+
/** Static command capabilities only; tasks cannot inject executables or shell prefixes. */
|
|
14
|
+
export const dagCommandCapabilitySchema = z.enum(["playwright-cli"]);
|
|
15
|
+
export const dagCommandPolicySchema = z.discriminatedUnion("mode", [
|
|
16
|
+
z.object({ mode: z.literal("deny") }).strict(),
|
|
17
|
+
z
|
|
18
|
+
.object({
|
|
19
|
+
mode: z.literal("capability-allowlist"),
|
|
20
|
+
capabilities: z.array(dagCommandCapabilitySchema).min(1),
|
|
21
|
+
})
|
|
22
|
+
.strict(),
|
|
23
|
+
]);
|
|
24
|
+
/** Undefined / omitted commandPolicy is treated as deny by executors and validators. */
|
|
25
|
+
export function resolveDagCommandPolicy(policy) {
|
|
26
|
+
return policy ?? { mode: "deny" };
|
|
27
|
+
}
|
|
28
|
+
export function dagCommandPolicyAllows(policy, capability) {
|
|
29
|
+
const resolved = resolveDagCommandPolicy(policy);
|
|
30
|
+
if (resolved.mode !== "capability-allowlist")
|
|
31
|
+
return false;
|
|
32
|
+
return resolved.capabilities.includes(capability);
|
|
33
|
+
}
|
|
13
34
|
export const dagOutputModeSchema = z.enum(["default", "structured-required"]);
|
|
14
35
|
export const dagShellPresetSchema = z.enum(["loop-agent-standard-verify"]);
|
|
15
36
|
export const dagVerifyQuotaSchema = z.enum(["1", "3", "full"]);
|
|
@@ -319,6 +340,7 @@ export const dagBackendTestPipelineSchema = z.enum([
|
|
|
319
340
|
"markdown-execute-html",
|
|
320
341
|
"markdown-manifest",
|
|
321
342
|
]);
|
|
343
|
+
export const dagFrontendBrowserToolPreflightSchema = z.object({}).strict();
|
|
322
344
|
export const dagFrontendTestEvidenceValidationSchema = z.object({}).strict();
|
|
323
345
|
export const dagFrontendTestCaseChecklistSchema = z.object({}).strict();
|
|
324
346
|
export const dagFrontendTestHtmlReportSchema = z.object({}).strict();
|
|
@@ -369,6 +391,7 @@ export const dagShellConfigSchema = z.object({
|
|
|
369
391
|
frontendVerificationBundle: dagFrontendVerificationBundleSchema.optional(),
|
|
370
392
|
frontendReviewContext: dagFrontendReviewContextSchema.optional(),
|
|
371
393
|
frontendTestCaseChecklist: dagFrontendTestCaseChecklistSchema.optional(),
|
|
394
|
+
frontendBrowserToolPreflight: dagFrontendBrowserToolPreflightSchema.optional(),
|
|
372
395
|
frontendTestEvidenceValidation: dagFrontendTestEvidenceValidationSchema.optional(),
|
|
373
396
|
frontendTestL5Report: z.object({}).strict().optional(),
|
|
374
397
|
frontendTestHtmlReport: dagFrontendTestHtmlReportSchema.optional(),
|
|
@@ -416,6 +439,7 @@ export const dagDynamicExpansionChildTaskSchema = z.object({
|
|
|
416
439
|
outputContract: z.string().optional(),
|
|
417
440
|
staticResultTemplate: z.string().optional(),
|
|
418
441
|
toolProfile: dagToolProfileSchema.optional(),
|
|
442
|
+
commandPolicy: dagCommandPolicySchema.optional(),
|
|
419
443
|
writePolicy: dagWritePolicySchema.optional(),
|
|
420
444
|
allowedPaths: z.array(z.string()).optional().default([]),
|
|
421
445
|
forbiddenPaths: z.array(z.string()).optional().default([]),
|
|
@@ -487,6 +511,7 @@ export const dagDynamicLoopBodyTaskSchema = z.object({
|
|
|
487
511
|
outputContract: z.string().optional(),
|
|
488
512
|
staticResultTemplate: z.string().optional(),
|
|
489
513
|
toolProfile: dagToolProfileSchema.optional(),
|
|
514
|
+
commandPolicy: dagCommandPolicySchema.optional(),
|
|
490
515
|
writePolicy: dagWritePolicySchema.optional(),
|
|
491
516
|
allowedPaths: z.array(z.string()).optional().default([]),
|
|
492
517
|
forbiddenPaths: z.array(z.string()).optional().default([]),
|
|
@@ -558,6 +583,8 @@ export const dagTaskSchema = z.object({
|
|
|
558
583
|
role: dagRoleSchema.optional(),
|
|
559
584
|
skills: z.array(z.string()).optional(),
|
|
560
585
|
toolProfile: dagToolProfileSchema.optional(),
|
|
586
|
+
/** Default deny: file write (toolProfile=write) does not grant command execution. */
|
|
587
|
+
commandPolicy: dagCommandPolicySchema.optional(),
|
|
561
588
|
writePolicy: dagWritePolicySchema.optional(),
|
|
562
589
|
writeSet: z.array(z.string()).optional(),
|
|
563
590
|
piStep: z.string().optional(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_DAG_EXECUTOR_MODELS, ENV_VAR_NAME_PATTERN, } from "./types.js";
|
|
1
|
+
import { DEFAULT_DAG_EXECUTOR_MODELS, ENV_VAR_NAME_PATTERN, dagCommandPolicyAllows, resolveDagCommandPolicy, } from "./types.js";
|
|
2
2
|
import { resolveShellCommands } from "../../executors/shell-executor.js";
|
|
3
3
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
4
4
|
import { resolveRepairTaskForGate } from "./repair-artifact.js";
|
|
@@ -461,6 +461,7 @@ function validateShellTaskConfig(task, spec, issues) {
|
|
|
461
461
|
!shell.jsonArtifactGate &&
|
|
462
462
|
!shell.backendTestPipeline &&
|
|
463
463
|
!shell.frontendPrewriteGate &&
|
|
464
|
+
!shell.frontendBrowserToolPreflight &&
|
|
464
465
|
!shell.frontendVerificationBundle &&
|
|
465
466
|
!shell.frontendReviewContext &&
|
|
466
467
|
!shell.frontendTestCaseChecklist &&
|
|
@@ -623,6 +624,104 @@ function validateWriterOutcomePolicyTaskConfig(task, issues) {
|
|
|
623
624
|
});
|
|
624
625
|
}
|
|
625
626
|
}
|
|
627
|
+
function isFrontendEvidenceWriteSet(writeSet) {
|
|
628
|
+
const entries = writeSet ?? [];
|
|
629
|
+
if (entries.length === 0)
|
|
630
|
+
return false;
|
|
631
|
+
return entries.every((entry) => {
|
|
632
|
+
const normalized = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
633
|
+
return (normalized === "testcase/frontend/evidence/**" ||
|
|
634
|
+
normalized.startsWith("testcase/frontend/evidence/"));
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
function isFrontendBrowserExecutorTask(task) {
|
|
638
|
+
const skills = task.skills ?? [];
|
|
639
|
+
return (task.executor === "pi" &&
|
|
640
|
+
task.toolProfile === "write" &&
|
|
641
|
+
skills.includes("playwright-cli") &&
|
|
642
|
+
isFrontendEvidenceWriteSet(task.writeSet));
|
|
643
|
+
}
|
|
644
|
+
function taskMentionsPlaywrightCliContract(task) {
|
|
645
|
+
const prompt = `${task.subtask_prompt}\n${task.outputContract ?? ""}`;
|
|
646
|
+
return /playwright[_-]?cli/i.test(prompt);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Command capability is orthogonal to file write. Fail closed when:
|
|
650
|
+
* - frontend browser executors lack playwright-cli capability
|
|
651
|
+
* - capability is granted without skill/prompt contract or outside evidence writeSet
|
|
652
|
+
* - non-browser nodes receive command capability
|
|
653
|
+
*/
|
|
654
|
+
function validateCommandPolicyTaskConfig(task, issues) {
|
|
655
|
+
const policy = resolveDagCommandPolicy(task.commandPolicy);
|
|
656
|
+
const allowsPlaywright = dagCommandPolicyAllows(task.commandPolicy, "playwright-cli");
|
|
657
|
+
const isBrowserExecutor = isFrontendBrowserExecutorTask(task);
|
|
658
|
+
if (isBrowserExecutor && !allowsPlaywright) {
|
|
659
|
+
issues.push({
|
|
660
|
+
type: "invalid-command-policy",
|
|
661
|
+
message: `frontend-test browser executor requires playwright-cli command capability (task ${task.id})`,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (policy.mode === "deny") {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (task.executor !== "pi") {
|
|
668
|
+
issues.push({
|
|
669
|
+
type: "invalid-command-policy",
|
|
670
|
+
message: `task ${task.id} commandPolicy requires executor=pi`,
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (allowsPlaywright) {
|
|
675
|
+
const skills = task.skills ?? [];
|
|
676
|
+
if (!skills.includes("playwright-cli")) {
|
|
677
|
+
issues.push({
|
|
678
|
+
type: "invalid-command-policy",
|
|
679
|
+
message: `task ${task.id} playwright-cli command capability requires skills to include playwright-cli`,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (!taskMentionsPlaywrightCliContract(task)) {
|
|
683
|
+
issues.push({
|
|
684
|
+
type: "invalid-command-policy",
|
|
685
|
+
message: `task ${task.id} playwright-cli command capability requires prompt/outputContract to reference playwright_cli`,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
if (!isFrontendEvidenceWriteSet(task.writeSet)) {
|
|
689
|
+
issues.push({
|
|
690
|
+
type: "invalid-command-policy",
|
|
691
|
+
message: `task ${task.id} playwright-cli command capability requires frontend evidence writeSet under testcase/frontend/evidence/`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (task.toolProfile !== "write") {
|
|
695
|
+
issues.push({
|
|
696
|
+
type: "invalid-command-policy",
|
|
697
|
+
message: `task ${task.id} playwright-cli command capability requires toolProfile=write for evidence mutation`,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function validateDynamicChildCommandPolicy(task, issues) {
|
|
703
|
+
const expansion = task.dynamicExpansion;
|
|
704
|
+
if (!expansion)
|
|
705
|
+
return;
|
|
706
|
+
const child = expansion.childTask;
|
|
707
|
+
const synthetic = {
|
|
708
|
+
id: `${task.id}-child-template`,
|
|
709
|
+
depends_on: [],
|
|
710
|
+
complexity: child.complexity,
|
|
711
|
+
subtask_prompt: child.subtaskPromptTemplate,
|
|
712
|
+
executor: child.executor,
|
|
713
|
+
role: child.role,
|
|
714
|
+
skills: child.skills,
|
|
715
|
+
toolProfile: child.toolProfile,
|
|
716
|
+
commandPolicy: child.commandPolicy,
|
|
717
|
+
writePolicy: child.writePolicy,
|
|
718
|
+
allowedPaths: child.allowedPaths,
|
|
719
|
+
forbiddenPaths: child.forbiddenPaths,
|
|
720
|
+
writeSet: child.writeSet,
|
|
721
|
+
outputContract: child.outputContract,
|
|
722
|
+
};
|
|
723
|
+
validateCommandPolicyTaskConfig(synthetic, issues);
|
|
724
|
+
}
|
|
626
725
|
function validateDecisionGateTaskConfig(task, issues) {
|
|
627
726
|
if (!task.decisionGate?.enabled) {
|
|
628
727
|
return;
|
|
@@ -727,6 +826,25 @@ export function collectForbiddenExecutorIssues(spec, forbiddenExecutors) {
|
|
|
727
826
|
implicitDefault: rawTasks[index]?.executor === undefined,
|
|
728
827
|
}));
|
|
729
828
|
}
|
|
829
|
+
/** Revalidate a rendered dynamic child before it enters state or executes. */
|
|
830
|
+
export function assertValidMaterializedDagTask(task) {
|
|
831
|
+
const issues = [];
|
|
832
|
+
// A minimal v2 carrier makes write-policy validation strict while avoiding
|
|
833
|
+
// unrelated parent graph/rank checks. Dynamic templates are validated at
|
|
834
|
+
// init; this guards values introduced by {{item}} rendering.
|
|
835
|
+
const spec = { version: 2, tasks: [task] };
|
|
836
|
+
validateTaskWritePolicy(task, spec, issues);
|
|
837
|
+
validateShellTaskConfig(task, spec, issues);
|
|
838
|
+
validateStaticTaskConfig(task, issues);
|
|
839
|
+
validateDecisionGateTaskConfig(task, issues);
|
|
840
|
+
validateRetryPolicyTaskConfig(task, issues);
|
|
841
|
+
validateOutputProtocolTaskConfig(task, issues);
|
|
842
|
+
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
843
|
+
validateCommandPolicyTaskConfig(task, issues);
|
|
844
|
+
if (issues.length > 0) {
|
|
845
|
+
throw new Error(`invalid materialized dynamic child ${task.id}: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
730
848
|
export function validateDagSpec(spec) {
|
|
731
849
|
const issues = [];
|
|
732
850
|
if (spec.tasks.length === 0) {
|
|
@@ -778,6 +896,8 @@ export function validateDagSpec(spec) {
|
|
|
778
896
|
validateRetryPolicyTaskConfig(task, issues);
|
|
779
897
|
validateOutputProtocolTaskConfig(task, issues);
|
|
780
898
|
validateWriterOutcomePolicyTaskConfig(task, issues);
|
|
899
|
+
validateCommandPolicyTaskConfig(task, issues);
|
|
900
|
+
validateDynamicChildCommandPolicy(task, issues);
|
|
781
901
|
validateProjectGovernanceTaskConfig(task, spec, issues);
|
|
782
902
|
validateFailureAwareDependsOn(task, spec, issues);
|
|
783
903
|
}
|
|
@@ -141,7 +141,7 @@ Runner / Loop ──(迁移中)──> 逐步改为仅经 Store / Appli
|
|
|
141
141
|
版本化自举包含两个不同的冻结层,不能用其中一个替代另一个:
|
|
142
142
|
|
|
143
143
|
| 层 | 冻结对象 | 生命周期 | 权威证据 |
|
|
144
|
-
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
145
|
| Controller identity | 实际发布包、entry/realEntry、launch spec、binary hash、portable package fingerprint | Feature/batch/Task/final verification | Worker、Task Pool、batch/Feature、QA/final evidence 中的 `controllerIdentity` |
|
|
146
146
|
| DAG skill snapshot | 某个 run 实际注入 prompt 的 ordered skill profiles、learned flag、budgets 与 dynamic bindings | 单个 DAG run,包括 pause/resume | `state.skillSnapshotRef` + `<runDir>/.runtime/skill-snapshot.json` |
|
|
147
147
|
|
|
@@ -150,7 +150,7 @@ Runner / Loop ──(迁移中)──> 逐步改为仅经 Store / Appli
|
|
|
150
150
|
**规则摘要**
|
|
151
151
|
|
|
152
152
|
| From | May import | Must not import |
|
|
153
|
-
|
|
153
|
+
| ------ | ------------ | ----------------- |
|
|
154
154
|
| `src/commands/**` | application, workflows, infrastructure, task, records, executors, shared | — |
|
|
155
155
|
| `src/workflows/**` | executors, task, records, shared, application(目标) | `src/commands/**` |
|
|
156
156
|
| `src/executors/**` | shared, 外部 SDK | `src/commands/**`, `src/cli/**` |
|
|
@@ -169,7 +169,7 @@ Runner / Loop ──(迁移中)──> 逐步改为仅经 Store / Appli
|
|
|
169
169
|
以下脚本由 `scripts/check-repo.sh` 调用(Phase 0 起):
|
|
170
170
|
|
|
171
171
|
| Script | 检查内容 | 失败条件 |
|
|
172
|
-
|
|
172
|
+
| -------- | ---------- | ---------- |
|
|
173
173
|
| `scripts/check-architecture-boundaries.sh` | workflow/executor forbidden import,及 Worker → CLI/commands/application import | 新的未 allowlist violation |
|
|
174
174
|
| `scripts/check-command-registry-drift.sh` | `command-reference.md` 中的 top-level command vs `src/cli/catalog.ts` | 文档引用未注册 command |
|
|
175
175
|
| `scripts/check-exec-plan-index-sync.sh` | `ai_workspace/loop-agent/exec-plans/{active,completed}` 目录文件 vs 对应 `README.md` 索引 | 任一 plan 文件未在索引登记 |
|
|
@@ -190,23 +190,25 @@ bash scripts/check-skill-entry.sh
|
|
|
190
190
|
|
|
191
191
|
Runtime 变更另需 `npm run typecheck` 及对应 targeted Vitest(见 exec plan 各 Phase 验证关口)。
|
|
192
192
|
|
|
193
|
-
## Client session
|
|
193
|
+
## Client session 恢复边界
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
主会话模型有两类常见缺口:**瞬态 UnknownError**(非标准 502 / `LLMRequestError` / 网络抖动 / 超时经 TypeValidation 落入 UnknownError,内置 APIError 重试不命中)与 **上下文过大**(含内网中文「请求上下文过大」及常见英文 overflow 文案,默认 overflow 识别不全)。loop-agent 通过 **职责分离的项目级产物** 补偿,不修改 OpenCode/Pi 上游,也不引入外部监督器,不改默认模型 `contextWindow`。
|
|
196
196
|
|
|
197
197
|
| 面 | 路径 / 入口 | 边界 |
|
|
198
|
-
|
|
199
|
-
| OpenCode
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| OpenCode transient-retry | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 仅瞬态 UnknownError resume;busy/retry 门控 + backoff;overflow/401/配额/取消/业务标 permanent(`plugin-ignore-permanent-error`),**不** compact |
|
|
200
|
+
| OpenCode overflow-compact | `.opencode/plugins/loop-agent-context-overflow-compact.js`(init surface `generated`) | 仅 context overflow → `session.compact`(无则 `summarize`)+ 有限 `promptAsync` 续接;`MAX_OVERFLOW_RECOVERIES ≤ 2`;session 锁;非 overflow 不动作 |
|
|
201
|
+
| Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;**不**写 `~/.pi` settings / compaction / 模型窗口 |
|
|
200
202
|
| Pi 用户配置 | `~/.pi/agent/settings.json` | **不**进入项目 `.harness/init-surface.json` hash;仅 `--client-recovery=user` 可字段级补缺并原子写;`auto`/`project`/`off` 与 `check-update` 默认零写 home;读取时只有 `ENOENT` 视为缺文件,其他 I/O 错误 fail closed |
|
|
201
|
-
| CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project`
|
|
202
|
-
| Ownership | recorded sha256 + apply-safe |
|
|
203
|
+
| CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project` 安装上述三份项目产物;`user` = 三产物 + 显式 Pi retry 合并;`off` 全跳过 |
|
|
204
|
+
| Ownership | recorded sha256 + apply-safe | 三份 generated 缺失可补、与 recorded hash 一致可升级;用户改过 → model merge / human decision,禁止静默覆盖 |
|
|
203
205
|
|
|
204
|
-
实现落点:`src/commands/client-recovery.ts
|
|
206
|
+
实现落点:`src/commands/client-recovery.ts`(共享 `isContextOverflow`、生成器与 install)+ `src/commands/init.ts`(surface 编排与 `written` 投影)。
|
|
205
207
|
|
|
206
208
|
## 演进里程碑
|
|
207
209
|
|
|
208
210
|
| Phase | 边界变化 |
|
|
209
|
-
|
|
211
|
+
| ------- | ---------- |
|
|
210
212
|
| 0(当前) | 文档 + 机器 guard;已知 loop/actions.ts 耦合 advisory |
|
|
211
213
|
| 1 | CLI command definition 单源 |
|
|
212
214
|
| 2 | Skill entry 瘦身 + frontmatter references |
|
|
@@ -166,7 +166,9 @@
|
|
|
166
166
|
"docs/templates/backend-test-execution.schema.json",
|
|
167
167
|
"docs/templates/backend-test-result.schema.json",
|
|
168
168
|
"docs/templates/backend-test-case-manifest.schema.json",
|
|
169
|
-
".opencode/plugins/loop-agent-transient-retry.js"
|
|
169
|
+
".opencode/plugins/loop-agent-transient-retry.js",
|
|
170
|
+
".opencode/plugins/loop-agent-context-overflow-compact.js",
|
|
171
|
+
".pi/extensions/loop-agent-context-overflow.js"
|
|
170
172
|
],
|
|
171
173
|
"initSurface": {
|
|
172
174
|
"README.md": "managed-block",
|
|
@@ -247,7 +249,9 @@
|
|
|
247
249
|
"docs/templates/backend-test-execution.schema.json": "copied",
|
|
248
250
|
"docs/templates/backend-test-result.schema.json": "copied",
|
|
249
251
|
"docs/templates/backend-test-case-manifest.schema.json": "copied",
|
|
250
|
-
".opencode/plugins/loop-agent-transient-retry.js": "generated"
|
|
252
|
+
".opencode/plugins/loop-agent-transient-retry.js": "generated",
|
|
253
|
+
".opencode/plugins/loop-agent-context-overflow-compact.js": "generated",
|
|
254
|
+
".pi/extensions/loop-agent-context-overflow.js": "generated"
|
|
251
255
|
},
|
|
252
256
|
"packageExcluded": [
|
|
253
257
|
"docs/progress/20*.md",
|
package/docs/templates/README.md
CHANGED
|
@@ -37,7 +37,15 @@
|
|
|
37
37
|
|
|
38
38
|
- `frontend-task-requirement.md`、`frontend-task-constraints.md`、`frontend-design-contract.md` — 前端任务输入与设计合同。
|
|
39
39
|
- `frontend-implementation-contract.schema.json` — 前端实现合同 schema。
|
|
40
|
-
- `frontend-test-dag.json` — frontend-test DAG
|
|
40
|
+
- `frontend-test-dag.json` — frontend-test DAG 模板;其 deterministic checklist 会拒绝 alternative executable instructions,仅允许 allowlisted `playwright-cli` command。Pi generator/reviser 的 runtime writeSet 仅授权下列目标项目生成路径(不是本仓库文档索引目标):
|
|
41
|
+
|
|
42
|
+
~~~text
|
|
43
|
+
testcase/frontend/cases/FE-*.md
|
|
44
|
+
testcase/frontend/cases/index.md
|
|
45
|
+
testcase/frontend/cases/manifest.draft.json
|
|
46
|
+
~~~
|
|
47
|
+
|
|
48
|
+
Pi writer writeSet 不包含 final manifest(文件名 manifest.json)。依赖 checklist 的 exclusive shell materializer 是 final manifest 的唯一写入者,以 temp+rename 原子生成 final,后续 map 只消费该 materializer 输出。
|
|
41
49
|
- `frontend-test-dag.retrieve-context.prompt.md`、`frontend-test-dag.generate-cases.prompt.md`、`frontend-test-dag.review-cases.prompt.md`、`frontend-test-dag.review-execution.prompt.md`、`frontend-test-dag.retrospect.prompt.md` — 上下文、用例、审查、执行审查和复盘提示。
|
|
42
50
|
- `frontend-test-case-checklist.md` — 前端测试用例检查清单。
|
|
43
51
|
|
|
@@ -14,15 +14,22 @@ Use `playwright-cli-case-generator`. Read only `testcase/frontend/rag/context.md
|
|
|
14
14
|
- `casePath` must be `testcase/frontend/cases/<caseId>.md`.
|
|
15
15
|
- `evidenceDir` must be `testcase/frontend/evidence/<caseId>/`.
|
|
16
16
|
|
|
17
|
-
Each case is independently executable and includes AC mapping (`acIds`), preconditions, cleanup,
|
|
17
|
+
Each case is independently executable and includes AC mapping (`acIds`), preconditions, cleanup, a semantic assertion, and its isolated evidence path. Use dimensions `core`, `boundary`, `flow`, or `backend`. Never guess API fields, constraints, SLA, credentials, or unrecorded data. For passed authority, include a successful `playwright-cli find ...` after `open` and before controller post-execution cleanup; `snapshot`, `goto`, `screenshot`, `request`/`console`, and ordinary interactions are not assertions.
|
|
18
18
|
|
|
19
|
-
Every case must
|
|
19
|
+
Every case must copy the **controller-frozen** absolute base URL supplied by the DAG. `testcase/frontend/rag/context.md` may reference that value, but model-authored context/case prose cannot establish or override the origin. Do not leave a base-url placeholder.
|
|
20
20
|
|
|
21
|
-
Every case must
|
|
21
|
+
Every case must start with `playwright-cli open --browser=chrome --headed` followed by the concrete controller-frozen URL supplied by the DAG. Do not copy an angle-bracket URL placeholder into a generated executable case line.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
### Dynamic element refs (hard)
|
|
24
|
+
|
|
25
|
+
- Executable `playwright-cli` lines must never contain angle-bracket dynamic-ref tokens such as `<fresh-ref>` or descriptive `<...>` placeholders.
|
|
26
|
+
- Use only shell-safe documentation placeholders `eX`, `eY`, ... for dynamic element refs. Each placeholder means the real `eNN` ref parsed from the immediately preceding latest `snapshot` output.
|
|
27
|
+
- Before each element interaction, write a fresh `snapshot` step. A later snapshot invalidates earlier refs: do not reuse a stale ref.
|
|
28
|
+
- `eX`/`eY` are documentation placeholders, not literal structured-tool arguments; the browser child resolves them to the current real `eNN` before its tool call.
|
|
29
|
+
|
|
30
|
+
At execution time the browser child translates each `playwright-cli` line into one structured `playwright_cli` tool call (no Bash). Keep Markdown steps as CLI lines so the executor can map them 1:1.
|
|
31
|
+
|
|
32
|
+
For file evidence, use canonical `--filename` only. For screenshots, use `playwright-cli screenshot --filename final.png`; when a real ref from the latest `snapshot` is needed, use `playwright-cli screenshot e5 --filename final.png`. For PDF use `playwright-cli pdf --filename final.pdf`. A `snapshot` without filename is response-only; for a file use `playwright-cli snapshot --filename snapshot.txt`. Never write `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or put an output path in a positional target slot.
|
|
26
33
|
|
|
27
34
|
Do not put a session flag before `open`. Every later Playwright CLI command must stay in that same default browser session: do **not** emit `-s=<case-id>`, `-s=...`, or assume an undocumented named-session binding.
|
|
28
35
|
|
|
@@ -45,4 +52,4 @@ Each case must require the executor to persist, even when blocked:
|
|
|
45
52
|
- `testcase/frontend/evidence/<case-id>/execution.md`
|
|
46
53
|
- `testcase/frontend/evidence/<case-id>/case-result.json`
|
|
47
54
|
|
|
48
|
-
`case-result.json` must be valid JSON containing the matching `caseId`, `status` (`passed`, `failed`, or `blocked`), and an `evidencePaths` array. A blocked result must include a non-empty `blockedReason`, such as `isolated-test-environment-unavailable` or `token-budget-exhausted`, and must never claim or imply a pass. `execution.md` records attempted or blocked steps, base-URL safety decision, fixture/reset and request-observation availability, timestamps, and the evidence-file list.
|
|
55
|
+
`case-result.json` must be valid JSON containing the matching `caseId`, `status` (`passed`, `failed`, or `blocked`), and an `evidencePaths` array. A blocked result must include a non-empty `blockedReason`, such as `isolated-test-environment-unavailable` or `token-budget-exhausted`, and must never claim or imply a pass. `execution.md` records attempted or blocked steps, base-URL safety decision, fixture/reset and request-observation availability, timestamps, and the evidence-file list. Available evidence files are required only when actually produced and must stay under the same case evidence directory.
|