@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.4
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 +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -98,6 +98,13 @@ export function isEphemeralToolCachePath(filePath) {
|
|
|
98
98
|
const normalized = normalizePath(filePath);
|
|
99
99
|
if (!normalized)
|
|
100
100
|
return false;
|
|
101
|
+
// CodeGraph extension sync side effect (plan 2026-08-21 D7): a loaded
|
|
102
|
+
// pi-codegraph extension keeps an existing .codegraph/ index fresh at
|
|
103
|
+
// session start; those index files are extension-owned run state, not
|
|
104
|
+
// writer output, so they never count as write-guard violations.
|
|
105
|
+
if (normalized === ".codegraph" || normalized.startsWith(".codegraph/")) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
101
108
|
if (normalized === ".pytest_cache" ||
|
|
102
109
|
normalized.startsWith(".pytest_cache/")) {
|
|
103
110
|
return true;
|
|
@@ -117,6 +124,13 @@ export function isEphemeralToolCachePath(filePath) {
|
|
|
117
124
|
if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
|
|
118
125
|
return true;
|
|
119
126
|
}
|
|
127
|
+
// vite/vitest runtime caches live under node_modules/.vite (e.g.
|
|
128
|
+
// node_modules/.vite/vitest/results.json). node_modules is the gitignored
|
|
129
|
+
// dependency tree — tool cache writes there are never a writer's product
|
|
130
|
+
// change and must not trip the write guard.
|
|
131
|
+
if (normalized.startsWith("node_modules/.vite/")) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
120
134
|
if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
|
|
121
135
|
return true;
|
|
122
136
|
if (/\.(?:pyc|pyo)$/.test(normalized))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal switch for the experimental "rerun failed node with an edited
|
|
3
|
+
* prompt" capability. Default is OFF; nothing may depend on it being enabled.
|
|
4
|
+
* The switch is deliberately env-based (no harness.json surface) while the
|
|
5
|
+
* feature is internal.
|
|
6
|
+
*/
|
|
7
|
+
export const DAG_RERUN_PROMPT_OVERRIDE_ENV = "HARNESS_DAG_RERUN_PROMPT_OVERRIDE";
|
|
8
|
+
/** Upper bound for an override input body (characters). */
|
|
9
|
+
export const MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS = 65_536;
|
|
10
|
+
export function isDagRerunPromptOverrideEnabled(env = process.env) {
|
|
11
|
+
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV];
|
|
12
|
+
return value === "1" || value === "true";
|
|
13
|
+
}
|
|
14
|
+
export const DAG_RERUN_PROMPT_ADDENDUM_TAG = "operator_addendum";
|
|
15
|
+
/**
|
|
16
|
+
* Compose the final subtask_prompt for a rerun override.
|
|
17
|
+
* - "replace": the operator text becomes the whole prompt (advanced mode).
|
|
18
|
+
* - "append" (recommended): the frozen base prompt is kept verbatim and the
|
|
19
|
+
* operator addendum is appended in a clearly marked block, mirroring the
|
|
20
|
+
* rerunFeedback / retry_instruction augmentation semantics — small diffs,
|
|
21
|
+
* no risk of dropping template scaffolding.
|
|
22
|
+
*/
|
|
23
|
+
export function composeDagPromptOverridePrompt(input) {
|
|
24
|
+
if (input.mode === "replace") {
|
|
25
|
+
return input.text;
|
|
26
|
+
}
|
|
27
|
+
return `${input.basePrompt}\n\n<${DAG_RERUN_PROMPT_ADDENDUM_TAG}>\n${input.text.trim()}\n</${DAG_RERUN_PROMPT_ADDENDUM_TAG}>\n`;
|
|
28
|
+
}
|
|
@@ -12,6 +12,19 @@ export const OPENSPEC_SPEC_DIRS = [
|
|
|
12
12
|
"openspec/project-specs",
|
|
13
13
|
"ai_workspace",
|
|
14
14
|
];
|
|
15
|
+
/** Default roots used when a frontend task does not declare specRoots. */
|
|
16
|
+
export const DEFAULT_FRONTEND_SPEC_ROOTS = OPENSPEC_SPEC_DIRS;
|
|
17
|
+
/** A task-owned spec root must be a normalized, repo-relative POSIX directory. */
|
|
18
|
+
export function isValidFrontendSpecRoot(value) {
|
|
19
|
+
return (Boolean(value) &&
|
|
20
|
+
!value.includes("\\") &&
|
|
21
|
+
!value.startsWith("/") &&
|
|
22
|
+
!value.includes("//") &&
|
|
23
|
+
!value.split("/").some((segment) => !segment ||
|
|
24
|
+
segment === "." ||
|
|
25
|
+
segment === ".." ||
|
|
26
|
+
/\s/.test(segment)));
|
|
27
|
+
}
|
|
15
28
|
/** Allowed spec file extensions (case-insensitive). */
|
|
16
29
|
export const OPENSPEC_SPEC_EXT_RE = /\.(md|mdx|json|yaml|yml)$/i;
|
|
17
30
|
/**
|
|
@@ -43,14 +56,17 @@ export function isGovernanceSubtreePath(filePath, governanceRoot) {
|
|
|
43
56
|
* directories. `filePath` must be a repo-relative POSIX path (backslashes are
|
|
44
57
|
* normalised internally). Directory names are case-sensitive.
|
|
45
58
|
*/
|
|
46
|
-
export function
|
|
59
|
+
export function isFrontendSpecPath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
|
|
47
60
|
const normalized = filePath.replaceAll("\\", "/");
|
|
48
|
-
for (const dir of
|
|
61
|
+
for (const dir of specRoots) {
|
|
49
62
|
if (normalized === dir || normalized.startsWith(dir + "/"))
|
|
50
63
|
return true;
|
|
51
64
|
}
|
|
52
65
|
return false;
|
|
53
66
|
}
|
|
67
|
+
export function isOpenspecSpecPath(filePath) {
|
|
68
|
+
return isFrontendSpecPath(filePath);
|
|
69
|
+
}
|
|
54
70
|
/**
|
|
55
71
|
* Returns `true` when `filename` (the basename only, not a full path) has an
|
|
56
72
|
* allowed frontend specification extension.
|
|
@@ -59,10 +75,13 @@ export function isValidOpenspecExtension(filename) {
|
|
|
59
75
|
return OPENSPEC_SPEC_EXT_RE.test(filename);
|
|
60
76
|
}
|
|
61
77
|
/** Returns `true` for a supported file inside a canonical openspec spec dir. */
|
|
62
|
-
export function
|
|
63
|
-
return (
|
|
78
|
+
export function isFrontendSpecFilePath(filePath, specRoots = DEFAULT_FRONTEND_SPEC_ROOTS) {
|
|
79
|
+
return (isFrontendSpecPath(filePath, specRoots) &&
|
|
64
80
|
isValidOpenspecExtension(filePath.replaceAll("\\", "/")));
|
|
65
81
|
}
|
|
82
|
+
export function isOpenspecSpecFilePath(filePath) {
|
|
83
|
+
return isFrontendSpecFilePath(filePath);
|
|
84
|
+
}
|
|
66
85
|
/**
|
|
67
86
|
* Returns `true` when free-form search input references a canonical frontend
|
|
68
87
|
* specification directory with exact lowercase directory names.
|
|
@@ -123,3 +142,50 @@ export function extractTaskSourceOpenspecPaths(markdown, governanceRoot = DEFAUL
|
|
|
123
142
|
!isGovernanceSubtreePath(token, governanceRoot))
|
|
124
143
|
.sort();
|
|
125
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract explicit specification-file citations for a task's frozen spec roots.
|
|
147
|
+
* This is intentionally path-only: source preparation decides which roots are
|
|
148
|
+
* normative, while DAG execution only consumes that frozen configuration.
|
|
149
|
+
*/
|
|
150
|
+
export function extractTaskSourceFrontendSpecPaths(markdown, specRoots, governanceRoot = DEFAULT_OPENSPEC_GOVERNANCE_ROOT) {
|
|
151
|
+
if (!markdown)
|
|
152
|
+
return [];
|
|
153
|
+
const raw = new Set();
|
|
154
|
+
const collect = (token) => {
|
|
155
|
+
const normalized = normalizeCitationToken(token);
|
|
156
|
+
if (normalized)
|
|
157
|
+
raw.add(normalized);
|
|
158
|
+
};
|
|
159
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g))
|
|
160
|
+
collect(match[1] ?? "");
|
|
161
|
+
for (const match of markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g))
|
|
162
|
+
collect(match[1] ?? "");
|
|
163
|
+
for (const root of specRoots) {
|
|
164
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
165
|
+
for (const match of markdown.matchAll(new RegExp(`(?:^|[\\s\"'([>])(${escaped}/[A-Za-z0-9._/-]+)`, "g"))) {
|
|
166
|
+
collect(match[1] ?? "");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return [...raw]
|
|
170
|
+
.filter((token) => isFrontendSpecFilePath(token, specRoots) && !isGovernanceSubtreePath(token, governanceRoot))
|
|
171
|
+
.sort();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Source-preparation helper for explicitly named normative directories. A root
|
|
175
|
+
* is inferred only on a line that declares a spec/规范/design-system intent;
|
|
176
|
+
* arbitrary Markdown links never silently broaden task configuration.
|
|
177
|
+
*/
|
|
178
|
+
export function extractDeclaredFrontendSpecRoots(markdown) {
|
|
179
|
+
const roots = new Set();
|
|
180
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
181
|
+
if (!/(?:规范|规约|标准|设计系统|\bspec(?:ification)?\b|\bdesign[ -]?system\b|\bguidelines?\b|\brules?\b)/i.test(line))
|
|
182
|
+
continue;
|
|
183
|
+
for (const match of line.matchAll(/`([^`]+)`|\[[^\]]+\]\(([^)\s]+)\)/g)) {
|
|
184
|
+
const raw = (match[1] ?? match[2] ?? "").replaceAll("\\", "/").replace(/\/+$/, "");
|
|
185
|
+
if (!raw || !isValidFrontendSpecRoot(raw))
|
|
186
|
+
continue;
|
|
187
|
+
roots.add(isValidOpenspecExtension(raw) ? raw.split("/").slice(0, -1).join("/") : raw);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [...roots].filter(isValidFrontendSpecRoot).sort();
|
|
191
|
+
}
|
|
@@ -1155,6 +1155,19 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1155
1155
|
modelCallable: "always",
|
|
1156
1156
|
humanConfirmation: "none",
|
|
1157
1157
|
},
|
|
1158
|
+
{
|
|
1159
|
+
action: "dagPromptOverrideConfig",
|
|
1160
|
+
cli: "console dag-prompt-override-config",
|
|
1161
|
+
kind: "read",
|
|
1162
|
+
inputSchemaVersion: 1,
|
|
1163
|
+
resultSchemaVersion: 1,
|
|
1164
|
+
envelopeSchemaVersion: 1,
|
|
1165
|
+
requiredErrorCodes: [],
|
|
1166
|
+
description: "Read-only probe of the experimental prompt-override rerun switch (env-gated, default off). The browser Recovery UI hides the failed-node prompt editor unless this returns enabled=true. Never mutates.",
|
|
1167
|
+
inputParams: [],
|
|
1168
|
+
modelCallable: "always",
|
|
1169
|
+
humanConfirmation: "none",
|
|
1170
|
+
},
|
|
1158
1171
|
{
|
|
1159
1172
|
action: "dagRerunPlan",
|
|
1160
1173
|
cli: "loop-agent dag rerun --run-id <id> --from-node <node> --plan --json",
|
|
@@ -1177,6 +1190,12 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1177
1190
|
required: true,
|
|
1178
1191
|
description: "node id to rerun from (prefer failed/attention node from dagReport primaryFailure.nodeId)",
|
|
1179
1192
|
},
|
|
1193
|
+
{
|
|
1194
|
+
name: "promptOverrides",
|
|
1195
|
+
type: "array",
|
|
1196
|
+
required: false,
|
|
1197
|
+
description: "experimental (switch off by default): [{nodeId, prompt, mode?}] — mode append (recommended) keeps the frozen base prompt and appends the text as a marked addendum block, replace swaps the whole subtask_prompt; only FAILED nodes inside the reset closure are admitted and the planHash binds the composed change",
|
|
1198
|
+
},
|
|
1180
1199
|
],
|
|
1181
1200
|
modelCallable: "always",
|
|
1182
1201
|
humanConfirmation: "none",
|
|
@@ -1219,6 +1238,12 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1219
1238
|
required: true,
|
|
1220
1239
|
description: "why continue from this node (e.g. provider flake; resume from review-pi)",
|
|
1221
1240
|
},
|
|
1241
|
+
{
|
|
1242
|
+
name: "promptOverrides",
|
|
1243
|
+
type: "array",
|
|
1244
|
+
required: false,
|
|
1245
|
+
description: "experimental (switch off by default): must match the dagRerunPlan promptOverrides exactly (same mode/text); the planHash already binds them, any drift is rejected",
|
|
1246
|
+
},
|
|
1222
1247
|
],
|
|
1223
1248
|
// always: fresh eligible dagRerun plans are model-callable (2026-08-11).
|
|
1224
1249
|
// The CLI keeps enforcing parent lifecycle, fromNode, planHash,
|
|
@@ -1822,6 +1847,13 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
1822
1847
|
source: "loop-agent",
|
|
1823
1848
|
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
1824
1849
|
},
|
|
1850
|
+
{
|
|
1851
|
+
command: "loop-agent init model-catalog",
|
|
1852
|
+
coverage: "excluded",
|
|
1853
|
+
action: null,
|
|
1854
|
+
source: "loop-agent",
|
|
1855
|
+
exclusionReason: "Not exposed to Operator Chat in M5.",
|
|
1856
|
+
},
|
|
1825
1857
|
{
|
|
1826
1858
|
command: "loop-agent init doctor",
|
|
1827
1859
|
coverage: "excluded",
|
|
@@ -9,6 +9,7 @@ export const PLAYWRIGHT_CLI_ALLOWED_COMMANDS = [
|
|
|
9
9
|
"goto",
|
|
10
10
|
"snapshot",
|
|
11
11
|
"find",
|
|
12
|
+
"find-unique",
|
|
12
13
|
"click",
|
|
13
14
|
"dblclick",
|
|
14
15
|
"fill",
|
|
@@ -35,6 +36,20 @@ export const PLAYWRIGHT_CLI_ALLOWED_COMMANDS = [
|
|
|
35
36
|
"requests",
|
|
36
37
|
"request",
|
|
37
38
|
];
|
|
39
|
+
/**
|
|
40
|
+
* Parse the match count from a playwright-cli find output. The CLI prints
|
|
41
|
+
* `Found N match(es) for "...":` on success and `No matches found for "...".`
|
|
42
|
+
* on zero — both with exit code 0, so the count is the only assertion signal.
|
|
43
|
+
* Returns undefined when the output shape is unrecognized (e.g. error pages).
|
|
44
|
+
*/
|
|
45
|
+
export function parsePlaywrightCliFindMatchCount(output) {
|
|
46
|
+
const found = /Found\s+(\d+)\s+match(?:es)?\s+for\b/i.exec(output);
|
|
47
|
+
if (found)
|
|
48
|
+
return Number(found[1]);
|
|
49
|
+
if (/No matches found for\b/i.test(output))
|
|
50
|
+
return 0;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
38
53
|
const allowedCommandSet = new Set(PLAYWRIGHT_CLI_ALLOWED_COMMANDS);
|
|
39
54
|
export function isPlaywrightCliCommand(value) {
|
|
40
55
|
return allowedCommandSet.has(value);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { acquireUpdateLock, readUpdateState, updateLockPath, updateStatePath, writeUpdateState, } from "./state.js";
|
|
4
|
+
export async function runConsoleDailyUpdateCheck(options) {
|
|
5
|
+
void options.writeStdout;
|
|
6
|
+
void options.packageRoot;
|
|
7
|
+
void options.cwd;
|
|
8
|
+
const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
|
|
9
|
+
const now = options.now ?? (() => new Date());
|
|
10
|
+
try {
|
|
11
|
+
if (!canPromptInteractively(options))
|
|
12
|
+
return;
|
|
13
|
+
const statePath = updateStatePath(options.homeDir);
|
|
14
|
+
const today = formatLocalDate(now());
|
|
15
|
+
const existing = await readUpdateState(statePath);
|
|
16
|
+
if (existing.lastPromptedLocalDate === today)
|
|
17
|
+
return;
|
|
18
|
+
const release = await acquireUpdateLock(updateLockPath(options.homeDir), {
|
|
19
|
+
now,
|
|
20
|
+
});
|
|
21
|
+
if (!release)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
const state = await readUpdateState(statePath);
|
|
25
|
+
if (state.lastPromptedLocalDate === today)
|
|
26
|
+
return;
|
|
27
|
+
const check = await options.client.latestVersionCheck();
|
|
28
|
+
if (!check.ok)
|
|
29
|
+
return;
|
|
30
|
+
if (!semver.gt(check.version, options.currentVersion))
|
|
31
|
+
return;
|
|
32
|
+
const parsedLatest = semver.parse(check.version);
|
|
33
|
+
if (!parsedLatest || parsedLatest.prerelease.length > 0)
|
|
34
|
+
return;
|
|
35
|
+
await markPromptedToday(statePath, state, today);
|
|
36
|
+
const answer = await ask(options, `loop-agent 有新版本:${options.currentVersion} → ${check.version}\n现在更新到 ${check.version}?[y/N] `);
|
|
37
|
+
if (!isYes(answer))
|
|
38
|
+
return;
|
|
39
|
+
const proof = await options.client.canAutoInstall();
|
|
40
|
+
if (!proof.ok) {
|
|
41
|
+
writeStderr(`当前 loop-agent 不是由此 npm 的正式全局安装管理,无法自动升级。\n请运行:npm install -g @tea-agent/loop-agent@${check.version}\n`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const install = await options.client.installExact(check.version);
|
|
45
|
+
if (!install.ok) {
|
|
46
|
+
writeStderr(`loop-agent 更新失败:${install.reason}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const verified = await options.client.verifyInstalled(check.version);
|
|
50
|
+
if (!verified.ok) {
|
|
51
|
+
writeStderr(`loop-agent 已尝试安装,但验证失败:${verified.reason}\n`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
writeStderr(`loop-agent 已更新到 ${check.version};新版本将在下一次命令调用时完全生效。\n`);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await release().catch(() => undefined);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
writeStderr(`loop-agent 更新检查已跳过:${error instanceof Error ? error.message : String(error)}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function canPromptInteractively(options) {
|
|
65
|
+
if (!options.stdinIsTTY || !options.stderrIsTTY)
|
|
66
|
+
return false;
|
|
67
|
+
if (options.env.CI)
|
|
68
|
+
return false;
|
|
69
|
+
if (options.env.LOOP_AGENT_DISABLE_UPDATE_CHECK === "1")
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function formatLocalDate(date) {
|
|
74
|
+
const year = date.getFullYear();
|
|
75
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
76
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
77
|
+
return `${year}-${month}-${day}`;
|
|
78
|
+
}
|
|
79
|
+
async function markPromptedToday(statePath, state, today) {
|
|
80
|
+
await writeUpdateState(statePath, {
|
|
81
|
+
...state,
|
|
82
|
+
lastPromptedLocalDate: today,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function ask(options, prompt) {
|
|
86
|
+
if (options.readLine) {
|
|
87
|
+
options.writeStderr?.(prompt);
|
|
88
|
+
return options.readLine(prompt);
|
|
89
|
+
}
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
91
|
+
try {
|
|
92
|
+
return await rl.question(prompt);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
rl.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isYes(answer) {
|
|
99
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
100
|
+
}
|
|
@@ -3,25 +3,48 @@ import { lstat, readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import semver from "semver";
|
|
5
5
|
const PACKAGE_NAME = "@tea-agent/loop-agent";
|
|
6
|
+
export const DEFAULT_NPM_QUERY_TIMEOUT_MS = 15_000;
|
|
6
7
|
export function createNpmUpdateClient(options) {
|
|
7
8
|
const npmCommand = options.npmCommand ?? "npm";
|
|
8
9
|
const loopAgentCommand = options.loopAgentCommand ?? "loop-agent";
|
|
9
10
|
const runner = options.runner ?? runCommand;
|
|
10
|
-
const timeoutMs = options.timeoutMs ??
|
|
11
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_NPM_QUERY_TIMEOUT_MS;
|
|
12
|
+
const cwd = options.cwd;
|
|
11
13
|
const env = options.env;
|
|
14
|
+
const latestVersionCheck = async () => {
|
|
15
|
+
let result;
|
|
16
|
+
try {
|
|
17
|
+
result = await runner(npmCommand, ["view", PACKAGE_NAME, "dist-tags.latest", "--json"], { timeoutMs, cwd, env });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { ok: false, reason: "spawn-failure" };
|
|
21
|
+
}
|
|
22
|
+
if (result.timedOut)
|
|
23
|
+
return { ok: false, reason: "timeout" };
|
|
24
|
+
if (result.exitCode === null) {
|
|
25
|
+
return { ok: false, reason: "spawn-failure" };
|
|
26
|
+
}
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
return { ok: false, reason: "command-failed" };
|
|
29
|
+
}
|
|
30
|
+
const version = parseNpmViewLatest(result.stdout);
|
|
31
|
+
return version
|
|
32
|
+
? { ok: true, version }
|
|
33
|
+
: { ok: false, reason: "invalid-response" };
|
|
34
|
+
};
|
|
12
35
|
return {
|
|
13
36
|
async latestVersion() {
|
|
14
|
-
const result = await
|
|
15
|
-
|
|
16
|
-
return undefined;
|
|
17
|
-
return parseNpmViewLatest(result.stdout);
|
|
37
|
+
const result = await latestVersionCheck();
|
|
38
|
+
return result.ok ? result.version : undefined;
|
|
18
39
|
},
|
|
40
|
+
latestVersionCheck,
|
|
19
41
|
async canAutoInstall() {
|
|
20
42
|
return proveGlobalNpmInstall({
|
|
21
43
|
currentVersion: options.currentVersion,
|
|
22
44
|
packageRoot: options.packageRoot,
|
|
23
45
|
npmCommand,
|
|
24
46
|
runner,
|
|
47
|
+
cwd,
|
|
25
48
|
env,
|
|
26
49
|
timeoutMs,
|
|
27
50
|
});
|
|
@@ -29,17 +52,25 @@ export function createNpmUpdateClient(options) {
|
|
|
29
52
|
async installExact(version) {
|
|
30
53
|
if (!isStrictRelease(version))
|
|
31
54
|
return { ok: false, reason: "invalid-version" };
|
|
32
|
-
const result = await runner(npmCommand, buildExactInstallArgs(version), {
|
|
55
|
+
const result = await runner(npmCommand, buildExactInstallArgs(version), {
|
|
56
|
+
timeoutMs: Math.max(timeoutMs, 120000),
|
|
57
|
+
cwd,
|
|
58
|
+
env,
|
|
59
|
+
});
|
|
33
60
|
if (result.exitCode !== 0 || result.timedOut)
|
|
34
61
|
return { ok: false, reason: "install-failed" };
|
|
35
62
|
return { ok: true };
|
|
36
63
|
},
|
|
37
64
|
async verifyInstalled(version) {
|
|
38
|
-
const list = await runner(npmCommand, ["list", "-g", PACKAGE_NAME, "--depth=0", "--json"], { timeoutMs, env });
|
|
65
|
+
const list = await runner(npmCommand, ["list", "-g", PACKAGE_NAME, "--depth=0", "--json"], { timeoutMs, cwd, env });
|
|
39
66
|
if (list.exitCode !== 0 || !verifyNpmListVersion(list.stdout, version)) {
|
|
40
67
|
return { ok: false, reason: "npm-list-version-mismatch" };
|
|
41
68
|
}
|
|
42
|
-
const pathVersion = await runner(loopAgentCommand, ["--version"], {
|
|
69
|
+
const pathVersion = await runner(loopAgentCommand, ["--version"], {
|
|
70
|
+
timeoutMs,
|
|
71
|
+
cwd,
|
|
72
|
+
env,
|
|
73
|
+
});
|
|
43
74
|
if (pathVersion.exitCode !== 0 || !verifyPathVersion(pathVersion.stdout, version)) {
|
|
44
75
|
return { ok: false, reason: "path-version-mismatch" };
|
|
45
76
|
}
|
|
@@ -80,6 +111,7 @@ export function verifyPathVersion(stdout, expectedVersion) {
|
|
|
80
111
|
async function proveGlobalNpmInstall(options) {
|
|
81
112
|
const root = await options.runner(options.npmCommand, ["root", "-g"], {
|
|
82
113
|
timeoutMs: options.timeoutMs,
|
|
114
|
+
cwd: options.cwd,
|
|
83
115
|
env: options.env,
|
|
84
116
|
});
|
|
85
117
|
if (root.exitCode !== 0 || root.timedOut)
|
|
@@ -113,6 +145,7 @@ function isStrictRelease(version) {
|
|
|
113
145
|
async function runCommand(command, args, options) {
|
|
114
146
|
return new Promise((resolve) => {
|
|
115
147
|
const child = spawn(command, [...args], {
|
|
148
|
+
cwd: options.cwd,
|
|
116
149
|
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
117
150
|
stdio: ["ignore", "pipe", "pipe"],
|
|
118
151
|
shell: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function updateStatePath(homeDir) {
|
|
4
4
|
return path.join(homeDir, ".loop-agent", "update-state.json");
|
|
@@ -18,6 +18,7 @@ export async function readUpdateState(statePath) {
|
|
|
18
18
|
latestVersion: stringOrUndefined(parsed.latestVersion),
|
|
19
19
|
dismissedVersion: stringOrUndefined(parsed.dismissedVersion),
|
|
20
20
|
retryAfter: stringOrUndefined(parsed.retryAfter),
|
|
21
|
+
lastPromptedLocalDate: stringOrUndefined(parsed.lastPromptedLocalDate),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
catch {
|
|
@@ -36,28 +37,55 @@ export async function acquireUpdateLock(lockPath, options = {}) {
|
|
|
36
37
|
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
37
38
|
try {
|
|
38
39
|
await mkdir(lockPath);
|
|
40
|
+
return await claimLock(lockPath, now);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
if (await isFreshLock(lockPath, now, staleMs))
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
47
|
+
await mkdir(lockPath);
|
|
48
|
+
return await claimLock(lockPath, now);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function claimLock(lockPath, now) {
|
|
56
|
+
try {
|
|
39
57
|
await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
|
|
40
58
|
return async () => {
|
|
41
59
|
await rm(lockPath, { recursive: true, force: true });
|
|
42
60
|
};
|
|
43
61
|
}
|
|
62
|
+
catch {
|
|
63
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function isFreshLock(lockPath, now, staleMs) {
|
|
68
|
+
try {
|
|
69
|
+
const ownerRaw = await readFile(path.join(lockPath, "owner.json"), "utf-8");
|
|
70
|
+
const owner = JSON.parse(ownerRaw);
|
|
71
|
+
if (typeof owner.at !== "string")
|
|
72
|
+
return false;
|
|
73
|
+
const at = Date.parse(owner.at);
|
|
74
|
+
if (!Number.isFinite(at))
|
|
75
|
+
return false;
|
|
76
|
+
return now().getTime() - at <= staleMs;
|
|
77
|
+
}
|
|
44
78
|
catch {
|
|
45
79
|
try {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
|
|
52
|
-
return async () => {
|
|
53
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
54
|
-
};
|
|
55
|
-
}
|
|
80
|
+
const info = await stat(lockPath);
|
|
81
|
+
const mtime = Number.isFinite(info.mtimeMs)
|
|
82
|
+
? info.mtimeMs
|
|
83
|
+
: info.mtime.getTime();
|
|
84
|
+
return now().getTime() - mtime <= staleMs;
|
|
56
85
|
}
|
|
57
86
|
catch {
|
|
58
|
-
return
|
|
87
|
+
return false;
|
|
59
88
|
}
|
|
60
|
-
return undefined;
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
function emptyState() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
|
|
3
3
|
export const taskFlowSchema = z.enum([
|
|
4
4
|
"auto",
|
|
5
5
|
"micro",
|
|
@@ -76,13 +76,27 @@ export const frontendOpenspecPolicySchema = z.enum(["cited", "scan-strict"]);
|
|
|
76
76
|
*/
|
|
77
77
|
export const frontendOpenspecConfigSchema = z.object({
|
|
78
78
|
policy: frontendOpenspecPolicySchema.optional().default("cited"),
|
|
79
|
+
/**
|
|
80
|
+
* Repo-relative directories containing frontend normative files. These are
|
|
81
|
+
* frozen in task.json during contract preparation, before DAG generation.
|
|
82
|
+
*/
|
|
83
|
+
specRoots: z
|
|
84
|
+
.array(z.string().refine((candidate) => isValidFrontendSpecRoot(candidate), "frontendOpenspec.specRoots entries must be normalized repo-relative directories"))
|
|
85
|
+
.optional(),
|
|
79
86
|
requiredReadPaths: z
|
|
80
|
-
.array(z
|
|
81
|
-
.string()
|
|
82
|
-
.refine((candidate) => !candidate.includes("\\") &&
|
|
83
|
-
isOpenspecSpecFilePath(candidate), "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under openspec/schemas/, openspec/project-specs/, or ai_workspace/"))
|
|
87
|
+
.array(z.string())
|
|
84
88
|
.optional()
|
|
85
89
|
.default([]),
|
|
90
|
+
}).superRefine((value, ctx) => {
|
|
91
|
+
const roots = value.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
|
|
92
|
+
if (new Set(roots).size !== roots.length) {
|
|
93
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.specRoots must not contain duplicates", path: ["specRoots"] });
|
|
94
|
+
}
|
|
95
|
+
for (const required of value.requiredReadPaths) {
|
|
96
|
+
if (!isFrontendSpecFilePath(required, roots)) {
|
|
97
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under frontendOpenspec.specRoots", path: ["requiredReadPaths"] });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
86
100
|
});
|
|
87
101
|
/** Batch limits and gate profile for the browser-driven frontend test DAG.
|
|
88
102
|
* Token fields are post-case stop thresholds, not model-provider hard caps. */
|
|
@@ -170,6 +170,9 @@ export function mergeManagedTaskConfigFields(existing, draft) {
|
|
|
170
170
|
command: cmd.command,
|
|
171
171
|
...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
|
|
172
172
|
})),
|
|
173
|
+
...(draft.frontendOpenspec !== undefined
|
|
174
|
+
? { frontendOpenspec: draft.frontendOpenspec }
|
|
175
|
+
: {}),
|
|
173
176
|
};
|
|
174
177
|
if (draft.featureId !== undefined) {
|
|
175
178
|
next.featureId = draft.featureId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { backendTestConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
|
|
2
|
+
import { backendTestConfigSchema, frontendOpenspecConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
|
|
3
3
|
import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
|
|
4
4
|
const sha256Schema = z
|
|
5
5
|
.string()
|
|
@@ -74,6 +74,7 @@ export const taskContractDraftSchema = z
|
|
|
74
74
|
openQuestions: z.array(z.string()).optional(),
|
|
75
75
|
assumptions: z.array(z.string()).optional(),
|
|
76
76
|
hardConstraints: z.array(z.string()).optional(),
|
|
77
|
+
frontendOpenspec: frontendOpenspecConfigSchema.optional(),
|
|
77
78
|
interviewAssessmentRef: z
|
|
78
79
|
.object({
|
|
79
80
|
draftSha256: z.string().min(1),
|