@tea-agent/loop-agent 0.39.0-beta.7 → 0.39.0-beta.9
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 +23 -5
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/commands/init.js +26 -0
- package/dist/executors/dag-pi-executor.js +326 -8
- package/dist/executors/pi-read-budget-policy.js +166 -0
- package/dist/executors/shell-executor.js +26 -30
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +15 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +123 -4
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +3 -0
- package/dist/worker/console/server.js +73 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-bPer1waP.js → abnfDiagram-N423BO3Z-D1-Z-BBa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BLdkwucW.js → arc-q8qAYm5m.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw4wo4I1.js → architectureDiagram-T3A2C74G-CAu4_lve.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DUUkc0gr.js → blockDiagram-VBNYF7ZC-D5GhQvHm.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BC3O0nob.js → c4Diagram-5PPSVZJV-ClQjeLkO.js} +1 -1
- package/dist/worker/console/static/assets/channel-ClAuhSUL.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Dd4g2nti.js → chunk-2GRJ4B5K-DLqWlFbo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-s2EDm7-n.js → chunk-2Q5K7J3B-DkRT_usL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-GqtHM2yQ.js → chunk-5RXB4S5H-Cqp7Chi3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BzRFrN8j.js → chunk-5VM5RSS4-CedEdFB9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bw9nFjCJ.js → chunk-6Q2QTUOP-BHM8vP5x.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B4n-03Qm.js → chunk-GF5L2VYU-D7EFr377.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dt75ZdBD.js → chunk-JWPE2WC7-DR38mTWV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B_afMhHB.js → chunk-KBJHAD2P-BFv2LsR3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Bd8Rqqg9.js → chunk-RYQCIY6F-BAvpJS5z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-9boOMji4.js → chunk-XXDRQBXY-eKI9TO72.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-cBgeCpVU.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BtCycfCc.js → cose-bilkent-JH36ORCC-BMJXpgo4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-eTOts9L1.js → cynefin-VYW2F7L2-CKJ2ofZx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cibd2mjg.js → cynefinDiagram-MW4NZA55-CsKnsazm.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CDjDZtDV.js → dagre-VZM6K2ZE-DVQ46vIw.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Btexn-JJ.js → diagram-7IWD3JNH-D_RuonYS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cyl8sdpM.js → diagram-B4RE2ZJO-D2Mt0nMu.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Ev-83E1t.js → diagram-LBJQPF4R-DjBDuye6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-41LqE0Mc.js → diagram-Q27KOJAE-8VZkqpE_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BC39T5R3.js → diagram-UB23O5K3-D0izYxia.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DzcNRuri.js → ebnfDiagram-BXEA7PRR-CncG2I95.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B5Frvvnz.js → erDiagram-JOGREHBK-DiBUUXR8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BapxROIM.js → flowDiagram-UKHOOZJN-Dtc0fArb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cd4d4wQ0.js → ganttDiagram-PKOTCBZU-BPNFP5OZ.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CrL6buFW.js → gitGraphDiagram-DS77QQ5N-CsgpxEEd.js} +1 -1
- package/dist/worker/console/static/assets/{index-DBh_x_Ek.js → index-Bai8Z25q.js} +81 -81
- package/dist/worker/console/static/assets/index-CH0Tr5hY.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BrSiNxyd.js → infoDiagram-6WML65LV-kHGxkJUS.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-gvlvY-n9.js → ishikawaDiagram-WSZJBQD7-CbLe9wrr.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-YaPzRQ.js → journeyDiagram-NVQOT4AX-BOkwxAcP.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BsmDv9E_.js → kanban-definition-27J2QSJJ-BHYQ2rDu.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UWaoDN3h.js → linear-CHT4ujob.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BlvpGVG8.js → mermaid.core-yL8sfqnY.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CUCIP5CP.js → mindmap-definition-FAOFIHXS-DZx1coV-.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CB1aQUAK.js → pegDiagram-VL7TDLO6-DStWRCI8.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BwehHxhf.js → pieDiagram-7S7Q4E2Y-DSKZkwzK.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CFz30APB.js → quadrantDiagram-CIZ2JOQS-CmoEGOuW.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwNmw1i6.js → railroadDiagram-AXF67PYL-BLqzhKyq.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-jg-tPXlI.js → requirementDiagram-LRYGKXZP-gbHeUabM.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BlyReDKl.js → sankeyDiagram-W5VNT64P-CaKkBCKO.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DBUzW1f3.js → sequenceDiagram-SI44F4Z6-PUbi3Upu.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BYcI2RAr.js → sizeCapture-X5ZJPWSS-dm7S9Qfe.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D7AiyJdo.js → stateDiagram-OKZ733FA-C7WSzv4p.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-sQDv5AP3.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BHGt7Kkv.js → swimlanes-SLNWSIFB-CBKCiQwd.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BXwsmcPs.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-OPjooWcX.js → timeline-definition-Z64GVDOM-CvnUlylk.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-ggsWDvrO.js → vennDiagram-T6HMQDX7-xM0StY1O.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DjNe-BjZ.js → wardleyDiagram-T6FBY63Y-CWtJMagy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Byp63Hei.js → xychartDiagram-ELKLHX3M-BO_-MeK8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/static/api.js +3 -3
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +5 -0
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/styles.css +331 -35
- package/dist/worker/observe/static/views/dag-inspector.js +509 -78
- package/dist/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/frontend-design-policy.js +19 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +117 -50
- package/dist/workflows/dag/frontend-plan-render.js +27 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +45 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +1 -0
- package/dist/workflows/dag/frontend-review-context.js +102 -15
- package/dist/workflows/dag/frontend-review-findings.js +42 -10
- package/dist/workflows/dag/frontend-shadow-dual-write.js +36 -2
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +154 -0
- package/dist/workflows/dag/frontend-writer-admission.js +12 -0
- package/dist/workflows/dag/init-hybrid.js +297 -65
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +111 -1
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/rerun-feedback.js +31 -8
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +34 -9
- package/dist/workflows/dag/runner.js +83 -16
- package/dist/workflows/dag/types.js +67 -11
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +4 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +3 -3
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +2 -3
- package/skills/frontend-implementation/references/design-spec.md +4 -3
- package/skills/frontend-implementation/references/node-contracts.md +4 -3
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-AbFtVnFm.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +0 -1
- package/dist/worker/console/static/assets/index-DwOGauxU.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +0 -8
|
@@ -16,6 +16,7 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
|
|
|
16
16
|
import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
|
|
17
17
|
import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
|
|
18
18
|
import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
|
|
19
|
+
import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
|
|
19
20
|
import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
|
|
20
21
|
import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
|
|
21
22
|
import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
|
|
@@ -27,7 +28,7 @@ import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate
|
|
|
27
28
|
import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
|
|
28
29
|
import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
|
|
29
30
|
import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.js";
|
|
30
|
-
|
|
31
|
+
const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
31
32
|
import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
|
|
32
33
|
import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
|
|
33
34
|
import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
|
|
@@ -2413,6 +2414,29 @@ async function executeFrontendDesignPolicy(input, meta) {
|
|
|
2413
2414
|
binding: meta.spec.sourceBinding,
|
|
2414
2415
|
});
|
|
2415
2416
|
}
|
|
2417
|
+
// Byte freshness is necessary but not sufficient: require the same ledger
|
|
2418
|
+
// semantic checks as the shadow prewrite gate before production writer
|
|
2419
|
+
// admission can be materialized.
|
|
2420
|
+
if (meta.spec.sourceBinding) {
|
|
2421
|
+
const ledgerCheck = await checkFrontendSourceFidelityLedger({
|
|
2422
|
+
workspaceRoot: input.cwd,
|
|
2423
|
+
binding: meta.spec.sourceBinding,
|
|
2424
|
+
contractRequirements: contract.requirements.map((requirement) => ({
|
|
2425
|
+
id: requirement.id,
|
|
2426
|
+
sourceFragmentIds: requirement.sourceFragmentIds,
|
|
2427
|
+
sourceRefs: requirement.sourceRefs,
|
|
2428
|
+
})),
|
|
2429
|
+
});
|
|
2430
|
+
if (!ledgerCheck.ok) {
|
|
2431
|
+
return {
|
|
2432
|
+
ok: false,
|
|
2433
|
+
stdout: "",
|
|
2434
|
+
stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
|
|
2435
|
+
failureCategory: "invalid-output",
|
|
2436
|
+
durationMs: Date.now() - started,
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2416
2440
|
const sourceFreshness = await collectFrontendSourceFreshness({
|
|
2417
2441
|
workspaceRoot: input.cwd,
|
|
2418
2442
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -2455,7 +2479,7 @@ async function executeFrontendDesignPolicy(input, meta) {
|
|
|
2455
2479
|
}
|
|
2456
2480
|
return {
|
|
2457
2481
|
ok: true,
|
|
2458
|
-
stdout: `Frontend design policy: approved findings=0\nContract: ${path.
|
|
2482
|
+
stdout: `Frontend design policy: approved findings=0\nContract: ${path.resolve(meta.runDir, config.outputDir, config.artifactName)}`,
|
|
2459
2483
|
stderr: "",
|
|
2460
2484
|
failureCategory: "success",
|
|
2461
2485
|
durationMs: Date.now() - started,
|
|
@@ -3377,34 +3401,6 @@ async function executeDagShellNodeInner(input, meta) {
|
|
|
3377
3401
|
if (shell?.frontendWriterAdmission) {
|
|
3378
3402
|
return executeFrontendWriterAdmission(input, meta);
|
|
3379
3403
|
}
|
|
3380
|
-
if (shell?.frontendPrewriteGate) {
|
|
3381
|
-
const started = Date.now();
|
|
3382
|
-
try {
|
|
3383
|
-
const result = await runFrontendPrewriteGate({
|
|
3384
|
-
runDir: meta.runDir,
|
|
3385
|
-
config: shell.frontendPrewriteGate,
|
|
3386
|
-
sourceBinding: meta.spec.sourceBinding,
|
|
3387
|
-
workspaceRoot: input.cwd,
|
|
3388
|
-
repoRoot: input.cwd,
|
|
3389
|
-
});
|
|
3390
|
-
return {
|
|
3391
|
-
ok: true,
|
|
3392
|
-
stdout: formatFrontendPrewriteGateStdout(result),
|
|
3393
|
-
stderr: "",
|
|
3394
|
-
failureCategory: "success",
|
|
3395
|
-
durationMs: Date.now() - started,
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
catch (error) {
|
|
3399
|
-
return {
|
|
3400
|
-
ok: false,
|
|
3401
|
-
stdout: "",
|
|
3402
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
3403
|
-
failureCategory: "invalid-output",
|
|
3404
|
-
durationMs: Date.now() - started,
|
|
3405
|
-
};
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
3404
|
if (shell?.frontendVerificationBundle) {
|
|
3409
3405
|
return executeFrontendVerificationBundle(input, meta);
|
|
3410
3406
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal switch for the experimental "rerun failed node with an edited
|
|
3
|
-
* prompt" capability.
|
|
4
|
-
*
|
|
5
|
-
* feature is internal.
|
|
3
|
+
* prompt" capability. The feature is enabled by default; the environment
|
|
4
|
+
* variable remains as an emergency kill switch while the feature is internal.
|
|
6
5
|
*/
|
|
7
6
|
export const DAG_RERUN_PROMPT_OVERRIDE_ENV = "HARNESS_DAG_RERUN_PROMPT_OVERRIDE";
|
|
8
7
|
/** Upper bound for an override input body (characters). */
|
|
9
8
|
export const MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS = 65_536;
|
|
10
9
|
export function isDagRerunPromptOverrideEnabled(env = process.env) {
|
|
11
|
-
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV];
|
|
12
|
-
return value
|
|
10
|
+
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV]?.trim().toLowerCase();
|
|
11
|
+
return value !== "0" && value !== "false" && value !== "off";
|
|
13
12
|
}
|
|
14
13
|
export const DAG_RERUN_PROMPT_ADDENDUM_TAG = "operator_addendum";
|
|
15
14
|
/**
|
|
@@ -11,10 +11,10 @@ export function createNpmUpdateClient(options) {
|
|
|
11
11
|
const timeoutMs = options.timeoutMs ?? DEFAULT_NPM_QUERY_TIMEOUT_MS;
|
|
12
12
|
const cwd = options.cwd;
|
|
13
13
|
const env = options.env;
|
|
14
|
-
const
|
|
14
|
+
const distTagVersionCheck = async (channel) => {
|
|
15
15
|
let result;
|
|
16
16
|
try {
|
|
17
|
-
result = await runner(npmCommand, ["view", PACKAGE_NAME,
|
|
17
|
+
result = await runner(npmCommand, ["view", PACKAGE_NAME, `dist-tags.${channel}`, "--json"], { timeoutMs, cwd, env });
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
return { ok: false, reason: "spawn-failure" };
|
|
@@ -27,17 +27,19 @@ export function createNpmUpdateClient(options) {
|
|
|
27
27
|
if (result.exitCode !== 0) {
|
|
28
28
|
return { ok: false, reason: "command-failed" };
|
|
29
29
|
}
|
|
30
|
-
const version =
|
|
30
|
+
const version = parseNpmViewDistTag(result.stdout, channel);
|
|
31
31
|
return version
|
|
32
32
|
? { ok: true, version }
|
|
33
33
|
: { ok: false, reason: "invalid-response" };
|
|
34
34
|
};
|
|
35
|
+
const latestVersionCheck = () => distTagVersionCheck("latest");
|
|
35
36
|
return {
|
|
36
37
|
async latestVersion() {
|
|
37
38
|
const result = await latestVersionCheck();
|
|
38
39
|
return result.ok ? result.version : undefined;
|
|
39
40
|
},
|
|
40
41
|
latestVersionCheck,
|
|
42
|
+
distTagVersionCheck,
|
|
41
43
|
async canAutoInstall() {
|
|
42
44
|
return proveGlobalNpmInstall({
|
|
43
45
|
currentVersion: options.currentVersion,
|
|
@@ -49,10 +51,11 @@ export function createNpmUpdateClient(options) {
|
|
|
49
51
|
timeoutMs,
|
|
50
52
|
});
|
|
51
53
|
},
|
|
52
|
-
async installExact(version) {
|
|
53
|
-
if (!
|
|
54
|
+
async installExact(version, channel = "latest") {
|
|
55
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
54
56
|
return { ok: false, reason: "invalid-version" };
|
|
55
|
-
|
|
57
|
+
}
|
|
58
|
+
const result = await runner(npmCommand, buildExactInstallArgs(version, channel), {
|
|
56
59
|
timeoutMs: Math.max(timeoutMs, 120000),
|
|
57
60
|
cwd,
|
|
58
61
|
env,
|
|
@@ -79,6 +82,13 @@ export function createNpmUpdateClient(options) {
|
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
export function parseNpmViewLatest(stdout) {
|
|
85
|
+
return parseNpmViewDistTag(stdout, "latest");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `latest` stays stable-only. `next` is explicitly selected by a human and
|
|
89
|
+
* may legitimately be a SemVer prerelease, but remains an exact version.
|
|
90
|
+
*/
|
|
91
|
+
export function parseNpmViewDistTag(stdout, channel) {
|
|
82
92
|
const trimmed = stdout.trim();
|
|
83
93
|
let value = trimmed;
|
|
84
94
|
try {
|
|
@@ -89,11 +99,12 @@ export function parseNpmViewLatest(stdout) {
|
|
|
89
99
|
}
|
|
90
100
|
if (typeof value !== "string")
|
|
91
101
|
return undefined;
|
|
92
|
-
return
|
|
102
|
+
return isAllowedChannelVersion(value, channel) ? value : undefined;
|
|
93
103
|
}
|
|
94
|
-
export function buildExactInstallArgs(version) {
|
|
95
|
-
if (!
|
|
96
|
-
throw new Error(`invalid
|
|
104
|
+
export function buildExactInstallArgs(version, channel = "latest") {
|
|
105
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
106
|
+
throw new Error(`invalid ${channel} version: ${version}`);
|
|
107
|
+
}
|
|
97
108
|
return ["install", "-g", `${PACKAGE_NAME}@${version}`, "--no-fund", "--no-audit"];
|
|
98
109
|
}
|
|
99
110
|
export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
@@ -106,7 +117,13 @@ export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
export function verifyPathVersion(stdout, expectedVersion) {
|
|
109
|
-
|
|
120
|
+
// `loop-agent --version` may append a build provenance marker. The release
|
|
121
|
+
// version itself must still match exactly; accepting no other trailing form
|
|
122
|
+
// keeps PATH verification bound to the requested stable package.
|
|
123
|
+
return new RegExp(`^${escapeRegExp(expectedVersion)}(?: \\(build [^)]+\\))?$`).test(stdout.trim());
|
|
124
|
+
}
|
|
125
|
+
function escapeRegExp(value) {
|
|
126
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
127
|
}
|
|
111
128
|
async function proveGlobalNpmInstall(options) {
|
|
112
129
|
const root = await options.runner(options.npmCommand, ["root", "-g"], {
|
|
@@ -142,6 +159,11 @@ function isStrictRelease(version) {
|
|
|
142
159
|
const parsed = semver.parse(version);
|
|
143
160
|
return Boolean(parsed && parsed.prerelease.length === 0);
|
|
144
161
|
}
|
|
162
|
+
function isAllowedChannelVersion(version, channel) {
|
|
163
|
+
if (channel === "latest")
|
|
164
|
+
return isStrictRelease(version);
|
|
165
|
+
return Boolean(semver.valid(version));
|
|
166
|
+
}
|
|
145
167
|
async function runCommand(command, args, options) {
|
|
146
168
|
return new Promise((resolve) => {
|
|
147
169
|
const child = spawn(command, [...args], {
|
|
@@ -219,6 +219,21 @@ const taskConfigObjectSchema = z.object({
|
|
|
219
219
|
maxFixLoops: z.number().int().min(0).optional().default(2),
|
|
220
220
|
/** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
|
|
221
221
|
convergence: convergenceConfigSchema.optional().default({ enabled: false }),
|
|
222
|
+
/** Frontend recovery continuation quota. Bounds how many times a failed
|
|
223
|
+
* frontend run may auto-recover via M6 recovery operations (1 = current
|
|
224
|
+
* default; raise to 2-3 for more tolerance of transient verify failures). */
|
|
225
|
+
frontendRecovery: z
|
|
226
|
+
.object({
|
|
227
|
+
maxContinuations: z
|
|
228
|
+
.number()
|
|
229
|
+
.int()
|
|
230
|
+
.min(1)
|
|
231
|
+
.max(5)
|
|
232
|
+
.optional()
|
|
233
|
+
.default(1),
|
|
234
|
+
})
|
|
235
|
+
.strict()
|
|
236
|
+
.optional(),
|
|
222
237
|
/** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
|
|
223
238
|
loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
|
|
224
239
|
.optional()
|
|
@@ -232,8 +232,11 @@ export async function applyTaskContract(input) {
|
|
|
232
232
|
// Re-parse through schema to ensure full TaskConfig shape for hash stability
|
|
233
233
|
const fullConfig = parseTaskConfigForRuntime(mergedConfig);
|
|
234
234
|
const taskConfigContent = serializeTaskConfigForDisk(fullConfig);
|
|
235
|
-
const requirementContent = projectRequirementMarkdown(input.draft);
|
|
235
|
+
const requirementContent = projectRequirementMarkdown(input.draft, input.requirementLedger);
|
|
236
236
|
const constraintsContent = projectConstraintsMarkdown(input.draft);
|
|
237
|
+
const requirementLedgerContent = input.requirementLedger !== undefined
|
|
238
|
+
? `${JSON.stringify(input.requirementLedger, null, 2)}\n`
|
|
239
|
+
: undefined;
|
|
237
240
|
// Validate references manifest hash if present (do not modify)
|
|
238
241
|
const paths = getTaskPaths(input.repoRoot, input.taskId);
|
|
239
242
|
const manifestText = await readFile(getSourceManifestPath(paths.sourceDir), "utf-8").catch(() => null);
|
|
@@ -241,6 +244,7 @@ export async function applyTaskContract(input) {
|
|
|
241
244
|
requirementBytes: requirementContent,
|
|
242
245
|
constraintsBytes: constraintsContent,
|
|
243
246
|
referenceManifestBytes: manifestText,
|
|
247
|
+
requirementLedgerBytes: requirementLedgerContent,
|
|
244
248
|
});
|
|
245
249
|
const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
|
|
246
250
|
const canonicalHash = computeCanonicalHash({
|
|
@@ -277,6 +281,9 @@ export async function applyTaskContract(input) {
|
|
|
277
281
|
constraintsContent,
|
|
278
282
|
taskConfigContent,
|
|
279
283
|
contractRef: nextRef,
|
|
284
|
+
...(requirementLedgerContent !== undefined
|
|
285
|
+
? { requirementLedgerContent }
|
|
286
|
+
: {}),
|
|
280
287
|
},
|
|
281
288
|
});
|
|
282
289
|
await appendRequestLedgerEntry({
|
|
@@ -312,6 +319,9 @@ export async function applyTaskContract(input) {
|
|
|
312
319
|
changedFiles: [
|
|
313
320
|
"source/需求.md",
|
|
314
321
|
"source/执行约束.md",
|
|
322
|
+
...(requirementLedgerContent !== undefined
|
|
323
|
+
? ["source/requirement-ledger.json"]
|
|
324
|
+
: []),
|
|
315
325
|
"task.json",
|
|
316
326
|
"contract.json",
|
|
317
327
|
],
|
|
@@ -29,3 +29,5 @@ export const TRANSACTIONS_DIRNAME = TRANSACTIONS_DIR_NAME;
|
|
|
29
29
|
export const REQUEST_LEDGER_BASENAME = REQUEST_LEDGER_FILE_NAME;
|
|
30
30
|
export const REQUIREMENT_FILE_NAME = "需求.md";
|
|
31
31
|
export const CONSTRAINTS_FILE_NAME = "执行约束.md";
|
|
32
|
+
/** Source fidelity ledger (20260824-frontend-source-fidelity-ledger-v1). */
|
|
33
|
+
export const REQUIREMENT_LEDGER_FILE_NAME = "requirement-ledger.json";
|
|
@@ -35,6 +35,9 @@ export function computeSourceHashes(input) {
|
|
|
35
35
|
if (input.referenceManifestBytes) {
|
|
36
36
|
sourceHashes.referenceManifestSha256 = sha256Utf8(input.referenceManifestBytes);
|
|
37
37
|
}
|
|
38
|
+
if (input.requirementLedgerBytes) {
|
|
39
|
+
sourceHashes.requirementLedgerSha256 = sha256Utf8(input.requirementLedgerBytes);
|
|
40
|
+
}
|
|
38
41
|
return sourceHashes;
|
|
39
42
|
}
|
|
40
43
|
export function refMatchesObserved(ref, observedCanonicalHash) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CONSTRAINTS_FILE_NAME, CONTRACT_FILE_NAME, REQUIREMENT_FILE_NAME, TRANSACTIONS_DIR_NAME, } from "./constants.js";
|
|
3
|
+
import { CONSTRAINTS_FILE_NAME, CONTRACT_FILE_NAME, REQUIREMENT_FILE_NAME, REQUIREMENT_LEDGER_FILE_NAME, TRANSACTIONS_DIR_NAME, } from "./constants.js";
|
|
4
4
|
import { computeCanonicalHash, computeSourceHashes, computeTaskConfigSha256, } from "./hash.js";
|
|
5
5
|
import { parseTaskContractRef } from "./schema.js";
|
|
6
6
|
import { getTaskPaths } from "../runtime.js";
|
|
@@ -33,11 +33,13 @@ export async function loadObservedTaskBytes(repoRoot, taskId) {
|
|
|
33
33
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
34
34
|
const requirementPath = path.join(paths.sourceDir, REQUIREMENT_FILE_NAME);
|
|
35
35
|
const constraintsPath = path.join(paths.sourceDir, CONSTRAINTS_FILE_NAME);
|
|
36
|
-
const
|
|
36
|
+
const requirementLedgerPath = path.join(paths.sourceDir, REQUIREMENT_LEDGER_FILE_NAME);
|
|
37
|
+
const [requirementBytes, constraintsBytes, taskConfigText, manifestText, requirementLedgerBytes] = await Promise.all([
|
|
37
38
|
readTextIfExists(requirementPath),
|
|
38
39
|
readTextIfExists(constraintsPath),
|
|
39
40
|
readTextIfExists(paths.taskConfigPath),
|
|
40
41
|
readTextIfExists(getSourceManifestPath(paths.sourceDir)),
|
|
42
|
+
readTextIfExists(requirementLedgerPath),
|
|
41
43
|
]);
|
|
42
44
|
let taskConfigRaw = null;
|
|
43
45
|
let taskConfigSha256 = null;
|
|
@@ -52,6 +54,7 @@ export async function loadObservedTaskBytes(repoRoot, taskId) {
|
|
|
52
54
|
taskConfigRaw,
|
|
53
55
|
taskConfigSha256,
|
|
54
56
|
referenceManifestBytes: manifestText,
|
|
57
|
+
requirementLedgerBytes,
|
|
55
58
|
requirementPath,
|
|
56
59
|
constraintsPath,
|
|
57
60
|
taskConfigPath: paths.taskConfigPath,
|
|
@@ -164,7 +167,9 @@ export async function observeTaskContractState(repoRoot, taskId) {
|
|
|
164
167
|
message: error instanceof Error ? error.message : String(error),
|
|
165
168
|
});
|
|
166
169
|
}
|
|
167
|
-
const observed = await computeObservedCanonicalHashSafe(repoRoot, taskId
|
|
170
|
+
const observed = await computeObservedCanonicalHashSafe(repoRoot, taskId, {
|
|
171
|
+
ref: ref ?? undefined,
|
|
172
|
+
});
|
|
168
173
|
errors.push(...(observed.errors ?? []));
|
|
169
174
|
if (!ref) {
|
|
170
175
|
return {
|
|
@@ -226,7 +231,7 @@ export async function observeTaskContractState(repoRoot, taskId) {
|
|
|
226
231
|
observationErrors: errors,
|
|
227
232
|
};
|
|
228
233
|
}
|
|
229
|
-
async function computeObservedCanonicalHashSafe(repoRoot, taskId) {
|
|
234
|
+
async function computeObservedCanonicalHashSafe(repoRoot, taskId, options) {
|
|
230
235
|
const errors = [];
|
|
231
236
|
try {
|
|
232
237
|
const bytes = await loadObservedTaskBytes(repoRoot, taskId);
|
|
@@ -256,10 +261,26 @@ async function computeObservedCanonicalHashSafe(repoRoot, taskId) {
|
|
|
256
261
|
}
|
|
257
262
|
return { hash: null, errors };
|
|
258
263
|
}
|
|
264
|
+
// Legacy compatibility (AC-FIDELITY-004): only include the ledger bytes
|
|
265
|
+
// when the stored ref actually declares requirementLedgerSha256. A ref
|
|
266
|
+
// without the ledger field keeps the existing semantics and never
|
|
267
|
+
// misreports drift for tasks that predate the ledger.
|
|
268
|
+
const refLedgerHash = options?.ref?.sourceHashes.requirementLedgerSha256 ?? null;
|
|
269
|
+
if (refLedgerHash && bytes.requirementLedgerBytes === null) {
|
|
270
|
+
errors.push({
|
|
271
|
+
code: "MISSING_REQUIREMENT_LEDGER",
|
|
272
|
+
path: "source/requirement-ledger.json",
|
|
273
|
+
message: `contract records requirementLedgerSha256 but ${REQUIREMENT_LEDGER_FILE_NAME} is missing`,
|
|
274
|
+
});
|
|
275
|
+
return { hash: null, errors };
|
|
276
|
+
}
|
|
259
277
|
const sourceHashes = computeSourceHashes({
|
|
260
278
|
requirementBytes: bytes.requirementBytes,
|
|
261
279
|
constraintsBytes: bytes.constraintsBytes,
|
|
262
280
|
referenceManifestBytes: bytes.referenceManifestBytes,
|
|
281
|
+
...(refLedgerHash
|
|
282
|
+
? { requirementLedgerBytes: bytes.requirementLedgerBytes }
|
|
283
|
+
: {}),
|
|
263
284
|
});
|
|
264
285
|
const hash = computeCanonicalHash({
|
|
265
286
|
sourceHashes,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { getTaskPaths } from "../runtime.js";
|
|
3
|
-
import { CONTRACT_JSON_BASENAME, REQUEST_LEDGER_BASENAME, TRANSACTIONS_DIRNAME, } from "./constants.js";
|
|
3
|
+
import { CONTRACT_JSON_BASENAME, REQUIREMENT_LEDGER_FILE_NAME, REQUEST_LEDGER_BASENAME, TRANSACTIONS_DIRNAME, } from "./constants.js";
|
|
4
4
|
export function getTaskContractPaths(repoRoot, taskId) {
|
|
5
5
|
const base = getTaskPaths(repoRoot, taskId);
|
|
6
6
|
return {
|
|
@@ -15,5 +15,6 @@ export function getTaskContractPaths(repoRoot, taskId) {
|
|
|
15
15
|
transactionsDir: path.join(base.taskDir, TRANSACTIONS_DIRNAME),
|
|
16
16
|
requestLedgerPath: path.join(base.taskDir, REQUEST_LEDGER_BASENAME),
|
|
17
17
|
lockPath: path.join(base.taskDir, ".contract.lock"),
|
|
18
|
+
requirementLedgerPath: path.join(base.sourceDir, REQUIREMENT_LEDGER_FILE_NAME),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { canonicalizePathArray, canonicalizeTextProjection, } from "./canonicalize.js";
|
|
2
2
|
/**
|
|
3
3
|
* Project Draft → source/需求.md (deterministic template, projectionVersion=1).
|
|
4
|
+
* 当携带 ledger 时追加 `## Source ledger` 区块:明确 需求.md 是派生导航视图,
|
|
5
|
+
* 语义权威为原始 requirement/acceptance 文档与受管 requirement-ledger.json。
|
|
4
6
|
*/
|
|
5
|
-
export function projectRequirementMarkdown(draft) {
|
|
7
|
+
export function projectRequirementMarkdown(draft, ledger) {
|
|
6
8
|
const lines = [
|
|
7
9
|
`# 需求:${draft.title}`,
|
|
8
10
|
"",
|
|
@@ -62,6 +64,18 @@ export function projectRequirementMarkdown(draft) {
|
|
|
62
64
|
lines.push(`- [${ref.role}] ${ref.ref}`);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
if (ledger) {
|
|
68
|
+
const gateStatus = ledger.stats.unresolvedCount > 0 ||
|
|
69
|
+
ledger.stats.conflictCount > 0 ||
|
|
70
|
+
ledger.stats.highRiskUncoveredCount > 0
|
|
71
|
+
? "blocked"
|
|
72
|
+
: "pass";
|
|
73
|
+
const ledgerLine = ledger.sourcePaths.length > 0
|
|
74
|
+
? `source/requirement-ledger.json (${ledger.sourcePaths.length} source document(s))`
|
|
75
|
+
: "source/requirement-ledger.json";
|
|
76
|
+
lines.push("", "## Source ledger", "", "> 本文件是派生导航视图,不替代原始 requirement/acceptance 文档。" +
|
|
77
|
+
" 语义权威为原始文档与 `source/requirement-ledger.json`(确定性切分账本)。", "", `- ledger: ${ledgerLine.length > 160 ? `${ledgerLine.slice(0, 160)}…` : ledgerLine}`, `- inputDigest: ${ledger.inputDigest}`, `- fragments: ${ledger.stats.fragmentCount}`, `- canonical requirements: ${ledger.stats.canonicalRequirementCount}`, `- status: ${gateStatus}`);
|
|
78
|
+
}
|
|
65
79
|
lines.push("");
|
|
66
80
|
return canonicalizeTextProjection(lines.join("\n"));
|
|
67
81
|
}
|
|
@@ -108,6 +108,8 @@ export const taskContractSourceHashesSchema = z
|
|
|
108
108
|
requirementSha256: sha256Schema,
|
|
109
109
|
constraintsSha256: sha256Schema,
|
|
110
110
|
referenceManifestSha256: sha256Schema.optional(),
|
|
111
|
+
/** Source fidelity ledger hash; present only when the task binds a ledger. */
|
|
112
|
+
requirementLedgerSha256: sha256Schema.optional(),
|
|
111
113
|
})
|
|
112
114
|
.strict();
|
|
113
115
|
export const taskContractRefSchema = z
|
|
@@ -21,6 +21,7 @@ export async function runJournaledContractCommit(input) {
|
|
|
21
21
|
const constraintsRel = `source/${CONSTRAINTS_FILE_NAME}`;
|
|
22
22
|
const taskConfigRel = "task.json";
|
|
23
23
|
const contractRel = CONTRACT_FILE_NAME;
|
|
24
|
+
const requirementLedgerRel = `source/requirement-ledger.json`;
|
|
24
25
|
const targetSpecs = [
|
|
25
26
|
{
|
|
26
27
|
kind: "requirement",
|
|
@@ -46,6 +47,17 @@ export async function runJournaledContractCommit(input) {
|
|
|
46
47
|
content: input.targets.contractRef,
|
|
47
48
|
isJson: true,
|
|
48
49
|
},
|
|
50
|
+
...(input.targets.requirementLedgerContent !== undefined
|
|
51
|
+
? [
|
|
52
|
+
{
|
|
53
|
+
kind: "requirement-ledger",
|
|
54
|
+
relativePath: requirementLedgerRel,
|
|
55
|
+
// Content is already serialized JSON text; stageText avoids double-encoding.
|
|
56
|
+
content: input.targets.requirementLedgerContent,
|
|
57
|
+
isJson: false,
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
49
61
|
];
|
|
50
62
|
const targetRecords = [];
|
|
51
63
|
for (const spec of targetSpecs) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, } from "../shared/openspec-spec.js";
|
|
9
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
|
|
10
10
|
const classifiedPathSchema = z
|
|
11
11
|
.string()
|
|
12
12
|
.refine((candidate) => OPENSPEC_SPEC_EXT_RE.test(candidate), "classified path must be a repo-relative supported spec file");
|
|
@@ -81,6 +81,77 @@ async function exists(filePath) {
|
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
export const FRONTEND_SPEC_REGISTRY_PATH = "ai_workspace/loop-agent/spec-registry.json";
|
|
85
|
+
const SPEC_SIGNAL_RE = /(?:规范|规约|标准|约定|guideline|standard|convention|design[- ]?system|rule|token|component|style|theme)/i;
|
|
86
|
+
const SPEC_EXCLUDE_RE = /(?:^|\/)(?:archive|history|changelog|changes|proposals?|release[-_ ]notes?)(?:\/|$)/i;
|
|
87
|
+
/** Stable, bounded heuristic used only for candidate ordering/classification. */
|
|
88
|
+
export function scoreFrontendSpecCandidate(filePath, contentPreview = "", options = {}) {
|
|
89
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
90
|
+
const lower = normalized.toLowerCase();
|
|
91
|
+
const reasons = [];
|
|
92
|
+
let score = 0;
|
|
93
|
+
if (options.registryMode === "normative") {
|
|
94
|
+
score += 100;
|
|
95
|
+
reasons.push("registry-normative");
|
|
96
|
+
}
|
|
97
|
+
else if (options.registryMode === "advisory") {
|
|
98
|
+
score += 30;
|
|
99
|
+
reasons.push("registry-advisory");
|
|
100
|
+
}
|
|
101
|
+
if (options.taskRelated) {
|
|
102
|
+
score += 25;
|
|
103
|
+
reasons.push("task-related");
|
|
104
|
+
}
|
|
105
|
+
if (/(?:design-system|guidelines?|standards?|conventions?|rules?|specs?)(?:\/|$)/i.test(lower)) {
|
|
106
|
+
score += 20;
|
|
107
|
+
reasons.push("spec-directory");
|
|
108
|
+
}
|
|
109
|
+
if (/(?:guidelines?|standards?|conventions?|rules?|tokens?|components?|theme|style)/i.test(path.basename(lower))) {
|
|
110
|
+
score += 15;
|
|
111
|
+
reasons.push("spec-filename");
|
|
112
|
+
}
|
|
113
|
+
if (SPEC_SIGNAL_RE.test(contentPreview)) {
|
|
114
|
+
score += 20;
|
|
115
|
+
reasons.push("spec-content");
|
|
116
|
+
}
|
|
117
|
+
if (/(?:^|\n)\s*(?:#{1,6}\s*)?(?:rules?|do|don't|必须|禁止|约定|规范)\b/im.test(contentPreview)) {
|
|
118
|
+
score += 15;
|
|
119
|
+
reasons.push("normative-structure");
|
|
120
|
+
}
|
|
121
|
+
if (SPEC_EXCLUDE_RE.test(lower)) {
|
|
122
|
+
score -= 80;
|
|
123
|
+
reasons.push("excluded-history-path");
|
|
124
|
+
}
|
|
125
|
+
const kind = options.registryMode === "normative" || score >= 80
|
|
126
|
+
? "normative"
|
|
127
|
+
: score >= 35
|
|
128
|
+
? "advisory"
|
|
129
|
+
: "unknown";
|
|
130
|
+
return { path: normalized, kind, score, reasons, source: options.registryMode ? "registry" : "auto-discovered" };
|
|
131
|
+
}
|
|
132
|
+
/** Read the optional project-level registry without making it mandatory. */
|
|
133
|
+
export async function readFrontendSpecRegistry(repoRoot) {
|
|
134
|
+
const raw = await readJson(path.join(repoRoot, FRONTEND_SPEC_REGISTRY_PATH));
|
|
135
|
+
if (!raw || typeof raw !== "object")
|
|
136
|
+
return null;
|
|
137
|
+
const value = raw;
|
|
138
|
+
if (value.version !== 1 || !Array.isArray(value.roots))
|
|
139
|
+
return null;
|
|
140
|
+
const roots = value.roots
|
|
141
|
+
.filter((item) => Boolean(item) && typeof item === "object")
|
|
142
|
+
.map((item) => ({
|
|
143
|
+
path: typeof item.path === "string" ? item.path.replaceAll("\\", "/").replace(/\/+$/, "") : "",
|
|
144
|
+
scope: Array.isArray(item.scope) ? item.scope.filter((v) => typeof v === "string") : undefined,
|
|
145
|
+
mode: (item.mode === "normative" || item.mode === "advisory" ? item.mode : undefined),
|
|
146
|
+
description: typeof item.description === "string" ? item.description : undefined,
|
|
147
|
+
}))
|
|
148
|
+
.filter((item) => isValidFrontendSpecRoot(item.path));
|
|
149
|
+
return {
|
|
150
|
+
version: 1,
|
|
151
|
+
roots,
|
|
152
|
+
discovery: value.discovery && typeof value.discovery === "object" ? value.discovery : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
84
155
|
/**
|
|
85
156
|
* Resolve only unambiguous symlink-root aliases. The physical target is never
|
|
86
157
|
* emitted as a candidate path: it merely lets task-source wording such as
|
|
@@ -133,6 +204,14 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
133
204
|
.filter(Boolean);
|
|
134
205
|
const collected = [];
|
|
135
206
|
let excludedGovernanceCount = 0;
|
|
207
|
+
let scannedFiles = 0;
|
|
208
|
+
let scannedBytes = 0;
|
|
209
|
+
const warnings = [];
|
|
210
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
211
|
+
const maxFiles = options.maxFiles ?? MAX_OPENSPEC_CANDIDATE_PATHS;
|
|
212
|
+
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
213
|
+
const timeoutMs = options.timeoutMs ?? 2000;
|
|
214
|
+
const startedAt = Date.now();
|
|
136
215
|
// Metadata / bookkeeping directories that are not normative design specs:
|
|
137
216
|
// changelogs, ADR/history archives, proposals under review, and scratch
|
|
138
217
|
// state. Reading them inflates the candidate list (and the plan prompt)
|
|
@@ -181,10 +260,19 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
181
260
|
excludedGovernanceCount,
|
|
182
261
|
truncated,
|
|
183
262
|
maxCandidates: MAX_OPENSPEC_CANDIDATE_PATHS,
|
|
263
|
+
warnings,
|
|
184
264
|
};
|
|
185
265
|
async function walk(dir, depth) {
|
|
186
|
-
if (
|
|
266
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
267
|
+
if (!warnings.includes("timeout-exceeded"))
|
|
268
|
+
warnings.push("timeout-exceeded");
|
|
187
269
|
return;
|
|
270
|
+
}
|
|
271
|
+
if (depth > maxDepth) {
|
|
272
|
+
if (!warnings.includes("max-depth-exceeded"))
|
|
273
|
+
warnings.push("max-depth-exceeded");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
188
276
|
let entries = [];
|
|
189
277
|
try {
|
|
190
278
|
entries = (await readdir(dir)).sort();
|
|
@@ -193,12 +281,30 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
|
|
|
193
281
|
return;
|
|
194
282
|
}
|
|
195
283
|
for (const name of entries) {
|
|
284
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
285
|
+
if (!warnings.includes("timeout-exceeded"))
|
|
286
|
+
warnings.push("timeout-exceeded");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (scannedFiles >= maxFiles) {
|
|
290
|
+
if (!warnings.includes("max-files-exceeded"))
|
|
291
|
+
warnings.push("max-files-exceeded");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
196
294
|
const full = path.join(dir, name);
|
|
197
295
|
try {
|
|
198
296
|
const info = await stat(full);
|
|
199
297
|
if (info.isDirectory())
|
|
200
298
|
await walk(full, depth + 1);
|
|
201
299
|
else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
|
|
300
|
+
const size = info.size;
|
|
301
|
+
if (scannedBytes + size > maxBytes) {
|
|
302
|
+
if (!warnings.includes("max-bytes-exceeded"))
|
|
303
|
+
warnings.push("max-bytes-exceeded");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
scannedFiles++;
|
|
307
|
+
scannedBytes += size;
|
|
202
308
|
const rel = path.relative(repoRoot, full).replace(/\\/g, "/");
|
|
203
309
|
if (isExcluded(rel)) {
|
|
204
310
|
excludedGovernanceCount++;
|
|
@@ -387,6 +493,7 @@ function toOpenspecDiscovery(discovery, designEvidence) {
|
|
|
387
493
|
excludedGovernanceCount: discovery.excludedGovernanceCount,
|
|
388
494
|
truncated: discovery.truncated,
|
|
389
495
|
maxCandidates: discovery.maxCandidates,
|
|
496
|
+
warnings: discovery.warnings,
|
|
390
497
|
};
|
|
391
498
|
}
|
|
392
499
|
export function buildAdapterGuidance(capability) {
|
|
@@ -424,12 +531,24 @@ export function buildAdapterGuidance(capability) {
|
|
|
424
531
|
* Strong evidence: package.json direct deps + config/bootstrap files.
|
|
425
532
|
*/
|
|
426
533
|
export async function discoverFrontendProjectCapability(repoRoot, options = {}) {
|
|
534
|
+
const registry = await readFrontendSpecRegistry(repoRoot);
|
|
535
|
+
const registryRoots = registry?.roots
|
|
536
|
+
.filter((root) => !root.scope || root.scope.includes("frontend"))
|
|
537
|
+
.map((root) => root.path) ?? [];
|
|
538
|
+
const discoveryOptions = {
|
|
539
|
+
...options,
|
|
540
|
+
specRoots: [...new Set([...OPENSPEC_SPEC_DIRS, ...(options.specRoots ?? []), ...registryRoots])],
|
|
541
|
+
maxDepth: registry?.discovery?.maxDepth ?? options.maxDepth,
|
|
542
|
+
maxFiles: registry?.discovery?.maxFiles ?? options.maxFiles,
|
|
543
|
+
maxBytes: registry?.discovery?.maxBytes ?? options.maxBytes,
|
|
544
|
+
timeoutMs: registry?.discovery?.timeoutMs ?? options.timeoutMs,
|
|
545
|
+
};
|
|
427
546
|
const evidencePaths = [];
|
|
428
547
|
const reasons = [];
|
|
429
548
|
const pkgPath = path.join(repoRoot, "package.json");
|
|
430
549
|
const pkgRaw = await readJson(pkgPath);
|
|
431
550
|
if (!pkgRaw || typeof pkgRaw !== "object") {
|
|
432
|
-
const discovery = await listOpenspecDiscovery(repoRoot,
|
|
551
|
+
const discovery = await listOpenspecDiscovery(repoRoot, discoveryOptions);
|
|
433
552
|
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
434
553
|
const base = {
|
|
435
554
|
schemaVersion: 1,
|
|
@@ -602,7 +721,7 @@ export async function discoverFrontendProjectCapability(repoRoot, options = {})
|
|
|
602
721
|
router = router ?? "next-router";
|
|
603
722
|
if (hasDep(deps, "vue-router"))
|
|
604
723
|
router = "vue-router";
|
|
605
|
-
const discovery = await listOpenspecDiscovery(repoRoot,
|
|
724
|
+
const discovery = await listOpenspecDiscovery(repoRoot, discoveryOptions);
|
|
606
725
|
const designEvidence = buildDesignEvidence(discovery.paths);
|
|
607
726
|
if (discovery.paths.length)
|
|
608
727
|
evidencePaths.push(...discovery.paths.slice(0, 5));
|