@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.19
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 +68 -2309
- package/README.md +7 -0
- package/dist/application/dag/args.js +8 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +2 -0
- package/dist/cli/program.js +3 -1
- package/dist/cli/update/init-surface-notifier.js +2 -0
- package/dist/cli/update/policy.js +2 -0
- package/dist/commands/cursor-prompt.js +2 -0
- package/dist/commands/dag-approve.js +2 -0
- package/dist/commands/dag-final-verification.js +2 -0
- package/dist/commands/dag-init-hybrid.js +2 -0
- package/dist/commands/dag-reconcile-tasks.js +2 -0
- package/dist/commands/dag-reject.js +2 -0
- package/dist/commands/dag-report.js +2 -0
- package/dist/commands/dag-rerun.js +2 -0
- package/dist/commands/dag-resume.js +2 -0
- package/dist/commands/dag-workflow-compile.js +2 -0
- package/dist/commands/dag-workflow-plan.js +2 -0
- package/dist/commands/dag-workflow-validate.js +2 -0
- package/dist/commands/delegate.js +2 -0
- package/dist/commands/examples.js +2 -0
- package/dist/commands/import-prd.js +2 -0
- package/dist/commands/init-model-catalog.js +2 -0
- package/dist/commands/init.js +19 -13
- package/dist/commands/instructions.js +2 -0
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/loop-benchmark.js +2 -0
- package/dist/commands/pi-prompt.js +2 -0
- package/dist/commands/pi-reuse-benchmark.js +2 -0
- package/dist/commands/promote-run.js +2 -0
- package/dist/commands/stats.js +1 -1
- package/dist/commands/study-init.js +2 -0
- package/dist/commands/task-advance.js +34 -0
- package/dist/commands/task-source-prepare.js +2 -0
- package/dist/commands/worktree-create.js +2 -0
- package/dist/commands/worktree-remove.js +2 -0
- package/dist/executors/dag-pi-executor.js +1640 -194
- package/dist/executors/pi-executor.js +85 -3
- package/dist/executors/pi-sdk-executor.js +18 -0
- package/dist/executors/shell-executor.js +92 -7
- package/dist/executors/shell-verification.js +3 -0
- package/dist/governance/exec-plans.js +6 -3
- package/dist/infrastructure/console/app-data.js +6 -0
- package/dist/shared/artifacts-core.js +2 -0
- package/dist/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/git-progress.js +2 -0
- package/dist/shared/one-shot-prompt-args.js +2 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/shared/package-metadata.js +6 -4
- package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
- package/dist/shared/reference-context.js +2 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/recover.js +6 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/artifact-meta.js +9 -2
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/task/source-prepare/fragment-inventory.js +4 -1
- package/dist/task/source-prepare/parse-intent.js +36 -11
- package/dist/task/task-demand-routing.js +5 -2
- package/dist/worker/console/chat/browser-automation.js +468 -308
- package/dist/worker/console/chat/browser-routes.js +175 -57
- package/dist/worker/console/chat/explore-tools.js +5 -2
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +115 -20
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
- package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
- package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
- package/dist/worker/console/chat/pi-runtime.js +95 -28
- package/dist/worker/console/chat/routes.js +20 -1
- package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
- package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
- package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
- package/dist/worker/console/chat/session-mode.js +8 -4
- package/dist/worker/console/chat/session-store.js +42 -9
- package/dist/worker/console/chat/tool-preview.js +115 -0
- package/dist/worker/console/chat/tools.js +7 -1
- package/dist/worker/console/chat/turn-order.js +13 -0
- package/dist/worker/console/chat/turn-process.js +32 -30
- package/dist/worker/console/operator-actions.js +50 -0
- package/dist/worker/console/prd-intake-bridge.js +54 -1
- package/dist/worker/console/scheduled-goal-host.js +98 -0
- package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
- package/dist/worker/console/server.js +15 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
- package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
- package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
- package/dist/worker/console/static/assets/{index-wXlQohNs.js → index-Cdkvw_H6.js} +97 -97
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
- package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
- package/dist/worker/console/static-src/prd-file-import.js +3 -1
- package/dist/worker/console/workspace-context.js +17 -1
- package/dist/worker/delivery/git-transaction.js +2 -0
- package/dist/worker/feature/fullstack-validate.js +2 -0
- package/dist/worker/loop-agent/loop-agent-client.js +8 -2
- package/dist/worker/materialize/harness-task-materializer.js +4 -1
- package/dist/worker/metrics/projector.js +1 -1
- package/dist/worker/observability/event-store.js +4 -1
- package/dist/worker/observability/read-model.js +4 -1
- package/dist/worker/observe/node-transparency.js +23 -1
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- package/dist/worker/outcomes/declared-artifacts.js +2 -0
- package/dist/worker/preflight.js +3 -0
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
- package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
- package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
- package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
- package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
- package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
- package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
- package/dist/worker/task-spec/validate.js +4 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
- package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
- package/dist/workflows/dag/dag-retry-schema.js +11 -0
- package/dist/workflows/dag/decision-envelope.js +2 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
- package/dist/workflows/dag/frontend-closeout.js +4 -2
- package/dist/workflows/dag/frontend-committed-facts.js +461 -0
- package/dist/workflows/dag/frontend-durable-tools.js +15 -3
- package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
- package/dist/workflows/dag/frontend-plan-canary.js +53 -0
- package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
- package/dist/workflows/dag/frontend-plan-render.js +0 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
- package/dist/workflows/dag/frontend-recovery-run.js +57 -0
- package/dist/workflows/dag/frontend-review-context.js +43 -70
- package/dist/workflows/dag/frontend-risk.js +92 -10
- package/dist/workflows/dag/frontend-session-budget.js +117 -3
- package/dist/workflows/dag/frontend-shape.js +11 -55
- package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
- package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
- package/dist/workflows/dag/frontend-verification-trace.js +25 -12
- package/dist/workflows/dag/frontend-writer-admission.js +3 -47
- package/dist/workflows/dag/frontend-writer-status.js +0 -23
- package/dist/workflows/dag/init-hybrid.js +202 -111
- package/dist/workflows/dag/interrupt-request.js +2 -0
- package/dist/workflows/dag/lifecycle.js +11 -2
- package/dist/workflows/dag/node-execution.js +50 -13
- package/dist/workflows/dag/reconcile-run.js +2 -0
- package/dist/workflows/dag/repair-artifact.js +3 -1
- package/dist/workflows/dag/report.js +2 -0
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +79 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +44 -6
- package/dist/workflows/dag/validate.js +4 -0
- package/docs/README.md +1 -0
- package/docs/governance/README.md +1 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +14 -10
- package/docs/templates/frontend-implementation-contract.schema.json +0 -7
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +8 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +8 -18
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +21 -25
- package/skills/frontend-review/references/review-findings.md +48 -24
- package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { deliverScheduledGoalResult } from "./chat/scheduled-goal-delivery.js";
|
|
2
|
+
import { ScheduledGoalHost } from "./scheduled-goal-host.js";
|
|
3
|
+
import { ScheduledGoalStore } from "../scheduler/scheduled-goal-store.js";
|
|
1
4
|
/**
|
|
2
5
|
* Repo-scoped Workspace Context (2026-08-22 multi-workspace foundation, W1).
|
|
3
6
|
*
|
|
@@ -298,7 +301,11 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
298
301
|
// Workspace-scoped git source control shared by the model tool (scm) and
|
|
299
302
|
// the Console UI SCM panel (HTTP API).
|
|
300
303
|
const scmService = new ScmService(repoRoot);
|
|
301
|
-
const browserAutomation = new BrowserAutomationService(
|
|
304
|
+
const browserAutomation = new BrowserAutomationService({
|
|
305
|
+
profileDir: appData.browserProfile,
|
|
306
|
+
downloadsDir: appData.browserDownloads,
|
|
307
|
+
screenshotsDir: appData.browserScreenshots,
|
|
308
|
+
});
|
|
302
309
|
const chatRuntime = new ConsolePiRuntime({
|
|
303
310
|
cwd: repoRoot,
|
|
304
311
|
sessionDir: appData.chatSessions,
|
|
@@ -336,6 +343,12 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
336
343
|
detachSession: (sessionId) => registry.detachSession(record.workspaceId, sessionId),
|
|
337
344
|
};
|
|
338
345
|
const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
|
|
346
|
+
const scheduledGoals = new ScheduledGoalStore(repoRoot);
|
|
347
|
+
const scheduledGoalHost = new ScheduledGoalHost({
|
|
348
|
+
goals: scheduledGoals, client, operations, events,
|
|
349
|
+
deliver: (goal) => deliverScheduledGoalResult(goal, scheduledGoals, chatStore),
|
|
350
|
+
onError: (goalId, error) => process.stderr.write(`[scheduled-goal] ${goalId ?? "wake"}: ${error instanceof Error ? error.message : String(error)}\n`),
|
|
351
|
+
});
|
|
339
352
|
const observeRouteContext = createObserveRouteContext(repoRoot, {
|
|
340
353
|
staticDir: deps.inspectStaticDir,
|
|
341
354
|
});
|
|
@@ -370,6 +383,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
370
383
|
if (disposed)
|
|
371
384
|
return;
|
|
372
385
|
disposed = true;
|
|
386
|
+
scheduledGoalHost.stop();
|
|
373
387
|
try {
|
|
374
388
|
goalRoundDriver.dispose();
|
|
375
389
|
}
|
|
@@ -388,6 +402,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
388
402
|
catch {
|
|
389
403
|
// best-effort
|
|
390
404
|
}
|
|
405
|
+
void browserAutomation.dispose();
|
|
391
406
|
try {
|
|
392
407
|
nightAuxTicker.dispose();
|
|
393
408
|
}
|
|
@@ -396,6 +411,7 @@ export async function createWorkspaceContext(record, deps) {
|
|
|
396
411
|
}
|
|
397
412
|
},
|
|
398
413
|
};
|
|
414
|
+
scheduledGoalHost.start();
|
|
399
415
|
return context;
|
|
400
416
|
}
|
|
401
417
|
/** Inspect-facing readiness/capability summary for a registry record. */
|
|
@@ -360,6 +360,8 @@ function matchesGlob(filePath, glob) {
|
|
|
360
360
|
let pattern = "^";
|
|
361
361
|
for (let index = 0; index < normalizedGlob.length; index += 1) {
|
|
362
362
|
const char = normalizedGlob[index];
|
|
363
|
+
if (char === undefined)
|
|
364
|
+
continue;
|
|
363
365
|
if (char === "*" && normalizedGlob[index + 1] === "*") {
|
|
364
366
|
pattern += ".*";
|
|
365
367
|
index += 1;
|
|
@@ -204,6 +204,8 @@ export function validateFullstackStructure(input) {
|
|
|
204
204
|
for (let j = i + 1; j < writerNodes.length; j++) {
|
|
205
205
|
const a = writerNodes[i];
|
|
206
206
|
const b = writerNodes[j];
|
|
207
|
+
if (!a || !b)
|
|
208
|
+
continue;
|
|
207
209
|
const aPaths = collectWritePaths(taskSpecs.get(a.id));
|
|
208
210
|
const bPaths = collectWritePaths(taskSpecs.get(b.id));
|
|
209
211
|
if (!writeSetsOverlap(aPaths, bPaths))
|
|
@@ -310,7 +310,10 @@ function findSourceScript(baseArgs, cwd) {
|
|
|
310
310
|
// Look for the last .ts/.js/.mjs/.cjs file in baseArgs
|
|
311
311
|
let scriptIdx = -1;
|
|
312
312
|
for (let i = baseArgs.length - 1; i >= 0; i--) {
|
|
313
|
-
const
|
|
313
|
+
const candidate = baseArgs[i];
|
|
314
|
+
if (candidate === undefined)
|
|
315
|
+
continue;
|
|
316
|
+
const ext = path.extname(candidate).toLowerCase();
|
|
314
317
|
if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
315
318
|
scriptIdx = i;
|
|
316
319
|
break;
|
|
@@ -318,7 +321,10 @@ function findSourceScript(baseArgs, cwd) {
|
|
|
318
321
|
}
|
|
319
322
|
if (scriptIdx < 0)
|
|
320
323
|
return undefined;
|
|
321
|
-
const
|
|
324
|
+
const scriptArg = baseArgs[scriptIdx];
|
|
325
|
+
if (scriptArg === undefined)
|
|
326
|
+
return undefined;
|
|
327
|
+
const scriptPath = path.resolve(cwd, scriptArg);
|
|
322
328
|
try {
|
|
323
329
|
const s = statSync(scriptPath);
|
|
324
330
|
if (s.isFile())
|
|
@@ -551,7 +551,10 @@ export function indexAcceptanceItems(raw) {
|
|
|
551
551
|
if (!match)
|
|
552
552
|
continue;
|
|
553
553
|
const id = match[1];
|
|
554
|
-
const
|
|
554
|
+
const body = match[2];
|
|
555
|
+
if (id === undefined || body === undefined)
|
|
556
|
+
continue;
|
|
557
|
+
const summary = compactSummary(body);
|
|
555
558
|
if (!map.has(id))
|
|
556
559
|
map.set(id, summary);
|
|
557
560
|
}
|
|
@@ -110,7 +110,7 @@ export async function projectMonthlyMetrics(input) {
|
|
|
110
110
|
}
|
|
111
111
|
function ratio(numerator, denominator, missingData) { return { numerator, denominator, value: denominator ? numerator / denominator : null, sampleSize: denominator, missingData }; }
|
|
112
112
|
function median(values) { if (!values.length)
|
|
113
|
-
return null; const middle = Math.floor(values.length / 2); return values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2; }
|
|
113
|
+
return null; const middle = Math.floor(values.length / 2); return values.length % 2 ? (values[middle] ?? 0) : ((values[middle - 1] ?? 0) + (values[middle] ?? 0)) / 2; }
|
|
114
114
|
function inWindow(value, start, end) { const time = new Date(value).getTime(); return time >= start.getTime() && time < end.getTime(); }
|
|
115
115
|
function dedupeRuns(runs) { return [...new Map(runs.map((run) => [run.workerRunId, run])).values()]; }
|
|
116
116
|
async function countBoundaryInterceptions(root, start, end) { let interceptions = 0; let audited = 0; try {
|
|
@@ -11,7 +11,10 @@ function serializeEvent(event) {
|
|
|
11
11
|
}
|
|
12
12
|
/** Identifiers used as event-store path segments must not alter the route. */
|
|
13
13
|
export function isSafeObservabilityIdentifier(value) {
|
|
14
|
-
return
|
|
14
|
+
return (typeof value === "string" &&
|
|
15
|
+
value !== "." &&
|
|
16
|
+
value !== ".." &&
|
|
17
|
+
!/[\\/\0]/.test(value));
|
|
15
18
|
}
|
|
16
19
|
export function createWorkerEventStore(jsonlPath) {
|
|
17
20
|
return {
|
|
@@ -1444,8 +1444,11 @@ async function loadFailureHandoffs(repoRoot) {
|
|
|
1444
1444
|
const match = /^(.+)-failure-closeout-draft\.md$/.exec(entry.name);
|
|
1445
1445
|
if (!match)
|
|
1446
1446
|
continue;
|
|
1447
|
+
const workerRunId = match[1];
|
|
1448
|
+
if (workerRunId === undefined)
|
|
1449
|
+
continue;
|
|
1447
1450
|
handoffs.push({
|
|
1448
|
-
workerRunId
|
|
1451
|
+
workerRunId,
|
|
1449
1452
|
closeoutDraftPath: path.join(handoffDir, entry.name),
|
|
1450
1453
|
});
|
|
1451
1454
|
}
|
|
@@ -106,6 +106,12 @@ export async function loadDagNodeContextSummary(repoRoot, dagRunId, nodeId, atte
|
|
|
106
106
|
body: { error: "Invalid node identifier", reason: "invalid-id" },
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
110
|
+
return {
|
|
111
|
+
status: 400,
|
|
112
|
+
body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
109
115
|
const loaded = await loadRunFacts(repoRoot, dagRunId);
|
|
110
116
|
if (!loaded.ok)
|
|
111
117
|
return loaded.failure;
|
|
@@ -242,6 +248,12 @@ export async function loadDagNodeArtifactInventory(repoRoot, dagRunId, nodeId) {
|
|
|
242
248
|
body: { error: "Invalid node identifier", reason: "invalid-id" },
|
|
243
249
|
};
|
|
244
250
|
}
|
|
251
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
252
|
+
return {
|
|
253
|
+
status: 400,
|
|
254
|
+
body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
245
257
|
const loaded = await loadRunFacts(repoRoot, dagRunId);
|
|
246
258
|
if (!loaded.ok)
|
|
247
259
|
return loaded.failure;
|
|
@@ -436,6 +448,8 @@ async function loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId)
|
|
|
436
448
|
}
|
|
437
449
|
}
|
|
438
450
|
export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, artifactId) {
|
|
451
|
+
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId))
|
|
452
|
+
return { status: 400, body: { error: "Invalid DAG run or node identifier", reason: "invalid-id" } };
|
|
439
453
|
const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
|
|
440
454
|
if (verified.status !== 200)
|
|
441
455
|
return verified;
|
|
@@ -448,6 +462,8 @@ export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, art
|
|
|
448
462
|
}
|
|
449
463
|
/** Full text uses the same binding and safety checks as the bounded preview. */
|
|
450
464
|
export async function loadDagNodeArtifactDownload(repoRoot, dagRunId, nodeId, artifactId) {
|
|
465
|
+
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId))
|
|
466
|
+
return { status: 400, body: { error: "Invalid DAG run or node identifier", reason: "invalid-id" } };
|
|
451
467
|
const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
|
|
452
468
|
if (verified.status !== 200)
|
|
453
469
|
return verified;
|
|
@@ -469,7 +485,7 @@ async function readValidatedContextReceipt(boundaryRoot, targetPath) {
|
|
|
469
485
|
return effectiveNodeContextV1Schema.safeParse(null);
|
|
470
486
|
}
|
|
471
487
|
export async function loadDagNodeContextComponent(repoRoot, dagRunId, nodeId, attempt, componentIndex) {
|
|
472
|
-
if (!isSafeObservabilityIdentifier(nodeId) || !Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(componentIndex) || componentIndex < 0)
|
|
488
|
+
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId) || !Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(componentIndex) || componentIndex < 0)
|
|
473
489
|
return { status: 400, body: { error: "Invalid context selection", reason: "invalid-id" } };
|
|
474
490
|
const loaded = await loadRunFacts(repoRoot, dagRunId);
|
|
475
491
|
if (!loaded.ok)
|
|
@@ -502,6 +518,12 @@ function stringField(value, key) {
|
|
|
502
518
|
return typeof value?.[key] === "string" ? value[key] : undefined;
|
|
503
519
|
}
|
|
504
520
|
export async function loadDagRunLineage(repoRoot, dagRunId) {
|
|
521
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
522
|
+
return {
|
|
523
|
+
status: 400,
|
|
524
|
+
body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
|
|
525
|
+
};
|
|
526
|
+
}
|
|
505
527
|
const loaded = await loadRunFacts(repoRoot, dagRunId);
|
|
506
528
|
if (!loaded.ok)
|
|
507
529
|
return loaded.failure;
|
|
@@ -835,6 +835,10 @@ async function handleDagNodeExecutionOutput(_req, res, match, ctx) {
|
|
|
835
835
|
const dagRunId = match.params.id;
|
|
836
836
|
const nodeId = match.params.sub;
|
|
837
837
|
const key = match.query.get("key");
|
|
838
|
+
if (dagRunId === undefined || nodeId === undefined) {
|
|
839
|
+
sendJson(res, 400, { error: "Invalid DAG node execution output selection", reason: "invalid-id" });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
838
842
|
// Reject path-shaped inputs even if smuggled via key/query aliases.
|
|
839
843
|
if (match.query.has("path") || match.query.has("artifactPath")) {
|
|
840
844
|
sendJson(res, 400, {
|
|
@@ -26,6 +26,7 @@ export const INSPECT_TABS = Object.freeze([
|
|
|
26
26
|
{ id: "dashboard", label: "总览", href: "/#/inspect" },
|
|
27
27
|
{ id: "pool", label: "资源池", href: "/#/inspect/pool" },
|
|
28
28
|
{ id: "failures", label: "异常 Task", href: "/#/inspect/failures" },
|
|
29
|
+
{ id: "scheduled-goals", label: "定时任务", href: "/#/inspect/scheduled-goals" },
|
|
29
30
|
]);
|
|
30
31
|
|
|
31
32
|
export const READINESS_LABELS = Object.freeze({
|
|
@@ -92,7 +93,7 @@ export function parseInspectObjectView(hashValue) {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Inspect 当前 hash 应高亮的工作区 tab href。
|
|
95
|
-
*
|
|
96
|
+
* 一级集合视图映射到各自 tab;下钻/对象页(dag/dags/run/night 等)
|
|
96
97
|
* 不高亮任何 tab——位置由面包屑与上下文 chip 表达(`#/dags` 自 0.33.0
|
|
97
98
|
* 起是独立历史分页页,不再是总览滚动别名)。
|
|
98
99
|
*/
|
|
@@ -106,6 +107,7 @@ export function activeInspectTabHref(hashValue) {
|
|
|
106
107
|
return "/#/inspect/pool";
|
|
107
108
|
}
|
|
108
109
|
if (internal === "/failures") return "/#/inspect/failures";
|
|
110
|
+
if (internal === "/scheduled-goals") return "/#/inspect/scheduled-goals";
|
|
109
111
|
if (internal.startsWith("/feature/")) return "/#/inspect";
|
|
110
112
|
return null;
|
|
111
113
|
}
|
|
@@ -12,6 +12,8 @@ export function matchesPathGlob(filePath, glob) {
|
|
|
12
12
|
let pattern = "^";
|
|
13
13
|
for (let index = 0; index < normalizedGlob.length; index += 1) {
|
|
14
14
|
const char = normalizedGlob[index];
|
|
15
|
+
if (char === undefined)
|
|
16
|
+
continue;
|
|
15
17
|
if (char === "*" && normalizedGlob[index + 1] === "*") {
|
|
16
18
|
pattern += ".*";
|
|
17
19
|
index += 1;
|
package/dist/worker/preflight.js
CHANGED
|
@@ -120,6 +120,9 @@ export async function preflightTargetRepo(input) {
|
|
|
120
120
|
}
|
|
121
121
|
if (input.runCheckRepo) {
|
|
122
122
|
const [command, ...args] = input.checkRepoCommand ?? ["bash", "scripts/check-repo.sh"];
|
|
123
|
+
if (command === undefined) {
|
|
124
|
+
throw new Error("check-repo command requires a binary");
|
|
125
|
+
}
|
|
123
126
|
const checkRepo = await input.client.runExternal(command, args, {
|
|
124
127
|
cwd: input.repoRoot,
|
|
125
128
|
artifactName: "check-repo",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getTaskPaths } from "../../task/runtime.js";
|
|
6
|
+
import { computeTaskConfigSha256 } from "../../task/contract/hash.js";
|
|
7
|
+
import { assessAutonomousExecutionEligibility, deriveEligibilityFactsFromDagSpec } from "../console/dag-execution-receipt.js";
|
|
8
|
+
import { scheduledGoalDraftSchema } from "./scheduled-goal-types.js";
|
|
9
|
+
const bindingSchema = z.object({
|
|
10
|
+
taskId: z.string(), revision: z.number().int().positive(), canonicalHash: z.string(),
|
|
11
|
+
taskConfigSha256: z.string(), projectionVersion: z.number(), canonicalizerVersion: z.number(), taskConfigSchemaVersion: z.number(),
|
|
12
|
+
});
|
|
13
|
+
const statusSchema = z.object({
|
|
14
|
+
taskId: z.string(), lifecycleState: z.literal("awaiting-write-set-approval"),
|
|
15
|
+
contract: z.object({ effectiveStatus: z.literal("managed"), revision: z.number().int().positive(),
|
|
16
|
+
canonicalHash: z.string(), observedCanonicalHash: z.string(), transactionIncomplete: z.literal(false),
|
|
17
|
+
taskConfigSha256: z.string(), projectionVersion: z.number(), canonicalizerVersion: z.number(), taskConfigSchemaVersion: z.number(), }),
|
|
18
|
+
dagDraft: z.object({ exists: z.literal(true), strictValidated: z.literal(true), specHash: z.string() }),
|
|
19
|
+
gate: z.object({ id: z.string().min(1), digest: z.string().min(1), type: z.literal("human-approval"),
|
|
20
|
+
bound: z.object({ taskId: z.string(), contractRevision: z.number(), contractCanonicalHash: z.string(), dagSpecHash: z.string() }), }),
|
|
21
|
+
activeRun: z.unknown().nullable().optional(), blockers: z.array(z.unknown()).length(0),
|
|
22
|
+
});
|
|
23
|
+
/** Freeze only a CLI-prepared managed task. This read-only intake never approves or starts a DAG. */
|
|
24
|
+
export async function prepareScheduledGoalDraft(input, client) {
|
|
25
|
+
// Validate caller-controlled identity before using it in CLI arguments or paths.
|
|
26
|
+
const parsed = scheduledGoalDraftSchema.omit({ prepared: true, controllerRef: true }).parse(input);
|
|
27
|
+
const identity = client.getIdentity();
|
|
28
|
+
if (!identity)
|
|
29
|
+
throw new Error("scheduled goal requires a pinned controller");
|
|
30
|
+
const root = await realpath(parsed.worktreeRef);
|
|
31
|
+
const paths = getTaskPaths(root, parsed.managedTaskId);
|
|
32
|
+
const readBound = async (file) => {
|
|
33
|
+
const resolved = await realpath(file);
|
|
34
|
+
const relative = path.relative(root, resolved);
|
|
35
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
36
|
+
throw new Error("scheduled goal prepared artifact escapes worktree");
|
|
37
|
+
return readFile(resolved, "utf8");
|
|
38
|
+
};
|
|
39
|
+
const status = await client.run(["task", "status", parsed.managedTaskId, "--json"], { cwd: root, expectJson: true, artifactName: `scheduled-goal-admission-${parsed.managedTaskId}-${randomUUID()}`, timeoutMs: 30_000 });
|
|
40
|
+
if (!status.ok || status.exitCode !== 0)
|
|
41
|
+
throw new Error("scheduled goal task status failed");
|
|
42
|
+
const envelope = z.object({ ok: z.literal(true), result: statusSchema }).parse(status.json);
|
|
43
|
+
const snapshot = envelope.result;
|
|
44
|
+
if (snapshot.taskId !== parsed.managedTaskId || snapshot.activeRun != null ||
|
|
45
|
+
snapshot.contract.canonicalHash !== snapshot.contract.observedCanonicalHash ||
|
|
46
|
+
snapshot.gate.bound.taskId !== parsed.managedTaskId ||
|
|
47
|
+
snapshot.gate.bound.contractRevision !== snapshot.contract.revision ||
|
|
48
|
+
snapshot.gate.bound.contractCanonicalHash !== snapshot.contract.canonicalHash)
|
|
49
|
+
throw new Error("scheduled goal managed contract binding mismatch");
|
|
50
|
+
const [dagBytes, configBytes] = await Promise.all([readBound(paths.dagDraftPath), readBound(paths.taskConfigPath)]);
|
|
51
|
+
const dagSpecHash = `sha256:${createHash("sha256").update(dagBytes).digest("hex")}`;
|
|
52
|
+
if (snapshot.dagDraft.specHash !== dagSpecHash || snapshot.gate.bound.dagSpecHash !== dagSpecHash)
|
|
53
|
+
throw new Error("scheduled goal validated DAG drift");
|
|
54
|
+
const dag = JSON.parse(dagBytes);
|
|
55
|
+
const binding = bindingSchema.parse(dag.taskContractBinding);
|
|
56
|
+
const expectedBinding = bindingSchema.parse({ ...snapshot.contract, taskId: parsed.managedTaskId });
|
|
57
|
+
if (dag.version !== 4 || JSON.stringify(binding) !== JSON.stringify(expectedBinding))
|
|
58
|
+
throw new Error("scheduled goal DAG contract binding mismatch");
|
|
59
|
+
const configJson = JSON.parse(configBytes);
|
|
60
|
+
if (computeTaskConfigSha256(configJson) !== binding.taskConfigSha256)
|
|
61
|
+
throw new Error("scheduled goal task configuration drift");
|
|
62
|
+
const config = z.object({ taskKind: z.string(), allowedPaths: z.array(z.string()), forbiddenPaths: z.array(z.string()) }).parse(configJson);
|
|
63
|
+
if (config.taskKind !== parsed.workflow || JSON.stringify(config.allowedPaths) !== JSON.stringify(parsed.allowedPaths) ||
|
|
64
|
+
JSON.stringify(config.forbiddenPaths) !== JSON.stringify(parsed.forbiddenPaths))
|
|
65
|
+
throw new Error("scheduled goal task scope or workflow mismatch");
|
|
66
|
+
const assessment = assessAutonomousExecutionEligibility(deriveEligibilityFactsFromDagSpec({
|
|
67
|
+
dagSpec: dag, allowedPaths: parsed.allowedPaths, forbiddenPaths: parsed.forbiddenPaths,
|
|
68
|
+
}));
|
|
69
|
+
if (!assessment.ok)
|
|
70
|
+
throw new Error(`scheduled goal ineligible: ${assessment.reason}`);
|
|
71
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
72
|
+
return scheduledGoalDraftSchema.parse({ ...parsed, controllerRef: identity.packageFingerprint.value,
|
|
73
|
+
prepared: { gateToken: `${snapshot.gate.id}:${snapshot.gate.digest}`,
|
|
74
|
+
dagPath: path.relative(root, paths.dagDraftPath), dagHash: digest(dagBytes),
|
|
75
|
+
contractPath: path.relative(root, paths.taskConfigPath), contractHash: digest(configBytes) }, });
|
|
76
|
+
}
|
|
77
|
+
/** Materialize only contract/runtime inputs; execution remains owned by the operation host. */
|
|
78
|
+
export async function prepareDerivedScheduledGoalTask(goalId, ownerToken, goals, client) {
|
|
79
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
80
|
+
const goal = await goals.get(goalId);
|
|
81
|
+
const entry = goal.supervisions.at(-1);
|
|
82
|
+
if (goal.state !== "admitted" || !entry || entry.ownerToken !== ownerToken || entry.state !== "preparing" || !entry.proposal)
|
|
83
|
+
throw new Error("derived preparation not authorized");
|
|
84
|
+
if (client.getIdentity()?.packageFingerprint.value !== goal.frozen.controllerRef)
|
|
85
|
+
throw new Error("derived controller drift");
|
|
86
|
+
const directory = path.join(goal.frozen.worktreeRef, ".harness", "scheduled-goal-inputs");
|
|
87
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
88
|
+
const prdPath = path.join(directory, `${entry.managedTaskId}.md`);
|
|
89
|
+
const content = ["# Scheduled goal continuation", "## Goal", goal.frozen.originalGoal,
|
|
90
|
+
"## Observed gap", entry.proposal.summary, "## Next task", entry.proposal.instructions,
|
|
91
|
+
"## Acceptance criteria", ...goal.frozen.acceptance.criteria.map((ac) => `- ${ac.acId}: ${ac.description}\n Verification: ${ac.command}`),
|
|
92
|
+
"## Scope", `Workflow: ${goal.frozen.workflow}; acceptance policy: ${goal.frozen.acceptance.policy}.`,
|
|
93
|
+
`Allowed paths: ${JSON.stringify(goal.frozen.allowedPaths)}. Forbidden paths: ${JSON.stringify(goal.frozen.forbiddenPaths)}.`,
|
|
94
|
+
"Preserve the current worktree changes. Do not change acceptance, dependencies, Git history, controller or runtime configuration. Do not push or merge.",
|
|
95
|
+
`Parent evidence: ${entry.parentEvidenceId}; tree: ${entry.treeHash}.`, ""].join("\n\n");
|
|
96
|
+
await writeFile(prdPath, content, { flag: "wx", mode: 0o600 });
|
|
97
|
+
const boundaryArgs = goal.frozen.allowedPaths.flatMap((ref) => ["--allowed-path", ref])
|
|
98
|
+
.concat(goal.frozen.forbiddenPaths.flatMap((ref) => ["--forbidden-path", ref]))
|
|
99
|
+
.concat(goal.frozen.acceptance.criteria.flatMap((ac) => ["--verify", `${ac.acId}:${ac.command}`]));
|
|
100
|
+
for (const [step, args] of [
|
|
101
|
+
["create", ["task", "advance", entry.managedTaskId, `Scheduled goal continuation ${entry.taskRevision}`, "--prd", prdPath, "--task-kind", goal.frozen.workflow, ...boundaryArgs, "--json"]],
|
|
102
|
+
["prepare", ["task", "advance", entry.managedTaskId, "--task-kind", goal.frozen.workflow, ...boundaryArgs, "--json"]],
|
|
103
|
+
]) {
|
|
104
|
+
const current = await goals.get(goalId);
|
|
105
|
+
if (current.state !== "admitted" || current.supervisions.at(-1)?.ownerToken !== ownerToken)
|
|
106
|
+
throw new Error("derived preparation authorization lost");
|
|
107
|
+
const remaining = Math.min(Date.parse(goal.frozen.time.stopAtUtc), Date.parse(goal.frozen.time.expiresAt)) - Date.now();
|
|
108
|
+
if (remaining <= 0)
|
|
109
|
+
throw new Error("derived preparation expired");
|
|
110
|
+
const result = await client.run([...args], { cwd: goal.frozen.worktreeRef, expectJson: true,
|
|
111
|
+
artifactName: `${entry.managedTaskId}-${step}`, timeoutMs: Math.min(remaining, 300000) });
|
|
112
|
+
if (!result.ok || result.exitCode !== 0 || result.timedOut)
|
|
113
|
+
throw new Error(`derived task ${step} failed or unknown`);
|
|
114
|
+
}
|
|
115
|
+
const { prepared: _prepared, controllerRef: _controller, ...input } = goal.frozen;
|
|
116
|
+
return prepareScheduledGoalDraft({ ...input, managedTaskId: entry.managedTaskId }, client);
|
|
117
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { lstat, readFile, readlink, realpath } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
|
+
import { scheduledGoalDraftSchema } from "./scheduled-goal-types.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
10
|
+
/** Run only frozen AC commands. A timeout is uncertain execution, never a passing or repairable failure. */
|
|
11
|
+
export async function evaluateScheduledGoalCriteria(frozen, options = {}) {
|
|
12
|
+
frozen = scheduledGoalDraftSchema.pick({ worktreeRef: true, frozenBase: true, allowedPaths: true, forbiddenPaths: true, acceptance: true, time: true }).strip().parse(frozen);
|
|
13
|
+
if (options.commandTimeoutMs !== undefined && (!Number.isFinite(options.commandTimeoutMs) || options.commandTimeoutMs <= 0))
|
|
14
|
+
throw new Error("invalid acceptance command timeout");
|
|
15
|
+
const now = options.now ?? (() => new Date());
|
|
16
|
+
const startedAt = now().toISOString();
|
|
17
|
+
const capture = () => captureScheduledGoalTree(frozen.worktreeRef, frozen.frozenBase, frozen.allowedPaths, frozen.forbiddenPaths);
|
|
18
|
+
const before = await capture();
|
|
19
|
+
const criteria = [];
|
|
20
|
+
let stopped = before.scopeViolations.length ? "scope_violation" : undefined;
|
|
21
|
+
let drift = false;
|
|
22
|
+
let executionUnknown = false;
|
|
23
|
+
for (const criterion of frozen.acceptance.criteria) {
|
|
24
|
+
if (options.mayRun && !await options.mayRun())
|
|
25
|
+
stopped ??= "assessment_cancelled";
|
|
26
|
+
const current = await capture();
|
|
27
|
+
if (current.treeHash !== before.treeHash) {
|
|
28
|
+
drift = true;
|
|
29
|
+
stopped = "tree_changed_during_acceptance";
|
|
30
|
+
}
|
|
31
|
+
const remaining = Math.min(Date.parse(frozen.time.stopAtUtc), Date.parse(frozen.time.expiresAt)) - now().getTime();
|
|
32
|
+
if (remaining <= 0)
|
|
33
|
+
stopped ??= "authorization_expired";
|
|
34
|
+
const result = { acId: criterion.acId, command: criterion.command,
|
|
35
|
+
status: "pending", exitCode: null, stdout: "", stderr: "", outputHash: sha256(JSON.stringify({ stdout: "", stderr: "", exitCode: null })), };
|
|
36
|
+
if (stopped)
|
|
37
|
+
result.reason = stopped;
|
|
38
|
+
else {
|
|
39
|
+
try {
|
|
40
|
+
const output = await execFileAsync("bash", ["-lc", criterion.command], {
|
|
41
|
+
cwd: frozen.worktreeRef, encoding: "utf8", maxBuffer: 1024 * 1024,
|
|
42
|
+
timeout: Math.max(1, Math.min(remaining, options.commandTimeoutMs ?? 120_000)),
|
|
43
|
+
});
|
|
44
|
+
result.stdout = output.stdout;
|
|
45
|
+
result.stderr = output.stderr;
|
|
46
|
+
result.exitCode = 0;
|
|
47
|
+
result.status = "succeeded";
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const failure = error;
|
|
51
|
+
result.stdout = failure.stdout ?? "";
|
|
52
|
+
result.stderr = failure.stderr ?? "";
|
|
53
|
+
if (typeof failure.code === "number" && !failure.killed && !failure.signal) {
|
|
54
|
+
result.exitCode = failure.code;
|
|
55
|
+
result.status = "failed";
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
executionUnknown = true;
|
|
59
|
+
result.reason = "command_execution_unknown";
|
|
60
|
+
stopped = result.reason;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
result.outputHash = sha256(JSON.stringify({ stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode }));
|
|
64
|
+
const verifiedTree = await capture();
|
|
65
|
+
if (verifiedTree.treeHash !== before.treeHash) {
|
|
66
|
+
drift = true;
|
|
67
|
+
stopped = "tree_changed_during_acceptance";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
criteria.push(result);
|
|
71
|
+
}
|
|
72
|
+
const after = await capture();
|
|
73
|
+
drift ||= before.treeHash !== after.treeHash;
|
|
74
|
+
if (drift)
|
|
75
|
+
for (const result of criteria) {
|
|
76
|
+
if (result.reason === "command_execution_unknown")
|
|
77
|
+
continue;
|
|
78
|
+
result.status = "stale";
|
|
79
|
+
result.reason = "tree_changed_during_acceptance";
|
|
80
|
+
}
|
|
81
|
+
const finishedAt = now().toISOString();
|
|
82
|
+
const expired = Date.parse(finishedAt) >= Math.min(Date.parse(frozen.time.stopAtUtc), Date.parse(frozen.time.expiresAt));
|
|
83
|
+
const status = executionUnknown ? "pending" : drift ? "stale" : before.scopeViolations.length || after.scopeViolations.length ? "failed"
|
|
84
|
+
: expired ? "pending"
|
|
85
|
+
: criteria.some((result) => result.status === "pending") ? "pending"
|
|
86
|
+
: criteria.every((result) => result.status === "succeeded") ? "succeeded" : "failed";
|
|
87
|
+
return { before, after, criteria, status, executionUnknown, startedAt, finishedAt };
|
|
88
|
+
}
|
|
89
|
+
/** Hash actual delivery bytes, including nonignored untracked files; staging is not a tree change. */
|
|
90
|
+
export async function captureScheduledGoalTree(worktree, base, allowedPaths, forbiddenPaths) {
|
|
91
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(base))
|
|
92
|
+
throw new Error("invalid frozen base");
|
|
93
|
+
const root = await realpath(worktree);
|
|
94
|
+
const git = async (...args) => (await execFileAsync("git", ["-C", root, ...args], {
|
|
95
|
+
encoding: "utf8", timeout: 30_000, maxBuffer: 16 * 1024 * 1024,
|
|
96
|
+
})).stdout;
|
|
97
|
+
const [head, top, tracked, untracked, baseline, format] = await Promise.all([
|
|
98
|
+
git("rev-parse", "HEAD"), git("rev-parse", "--show-toplevel"),
|
|
99
|
+
git("ls-files", "-z", "--cached"), git("ls-files", "-z", "--others", "--exclude-standard"),
|
|
100
|
+
git("ls-tree", "-r", "-z", base), git("rev-parse", "--show-object-format"),
|
|
101
|
+
]);
|
|
102
|
+
if (await realpath(top.trim()) !== root || head.trim() !== base)
|
|
103
|
+
throw new Error("scheduled goal worktree/base drift");
|
|
104
|
+
const entries = (value) => value.split("\0").filter(Boolean);
|
|
105
|
+
const objectFormat = format.trim();
|
|
106
|
+
if (objectFormat !== "sha1" && objectFormat !== "sha256")
|
|
107
|
+
throw new Error("unsupported Git object format");
|
|
108
|
+
const baseEntries = new Map(entries(baseline).map((entry) => {
|
|
109
|
+
const separator = entry.indexOf("\t");
|
|
110
|
+
const [mode, kind, objectId] = entry.slice(0, separator).split(" ");
|
|
111
|
+
if (separator < 0 || !mode || !kind || !objectId)
|
|
112
|
+
throw new Error("invalid Git tree entry");
|
|
113
|
+
return [entry.slice(separator + 1), { mode, kind, objectId }];
|
|
114
|
+
}));
|
|
115
|
+
const blobId = (bytes) => createHash(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
|
|
116
|
+
const names = [...new Set([...entries(tracked), ...entries(untracked), ...baseEntries.keys()])].sort();
|
|
117
|
+
const deliveryNames = new Set(names);
|
|
118
|
+
const files = [];
|
|
119
|
+
const changedPaths = [];
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
const expected = baseEntries.get(name);
|
|
122
|
+
let actualId;
|
|
123
|
+
let actualMode;
|
|
124
|
+
const target = path.resolve(root, name);
|
|
125
|
+
const relative = path.relative(root, target);
|
|
126
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
127
|
+
throw new Error("tree path escapes worktree");
|
|
128
|
+
try {
|
|
129
|
+
const parent = await realpath(path.dirname(target));
|
|
130
|
+
if (parent !== root && !parent.startsWith(`${root}${path.sep}`))
|
|
131
|
+
throw new Error("tree parent escapes worktree");
|
|
132
|
+
const stat = await lstat(target);
|
|
133
|
+
if (stat.isSymbolicLink()) {
|
|
134
|
+
const link = await readlink(target);
|
|
135
|
+
const resolved = await realpath(target).catch(() => { throw new Error(`unresolved delivery symlink: ${name}`); });
|
|
136
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
|
|
137
|
+
throw new Error(`delivery symlink escapes worktree: ${name}`);
|
|
138
|
+
if (!deliveryNames.has(path.relative(root, resolved)))
|
|
139
|
+
throw new Error(`delivery symlink target is not fingerprinted: ${name}`);
|
|
140
|
+
files.push({ name, kind: "symlink", hash: sha256(link) });
|
|
141
|
+
actualId = blobId(Buffer.from(link));
|
|
142
|
+
actualMode = "120000";
|
|
143
|
+
}
|
|
144
|
+
else if (stat.isFile()) {
|
|
145
|
+
const bytes = await readFile(target);
|
|
146
|
+
files.push({ name, kind: "file", hash: sha256(bytes), executable: Boolean(stat.mode & 0o111) });
|
|
147
|
+
actualId = blobId(bytes);
|
|
148
|
+
actualMode = stat.mode & 0o111 ? "100755" : "100644";
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
throw new Error(`unsupported delivery entry: ${name}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code === "ENOENT") {
|
|
155
|
+
if (expected)
|
|
156
|
+
files.push({ name, kind: "missing" });
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (expected ? expected.objectId !== actualId || expected.mode !== actualMode : actualId !== undefined)
|
|
162
|
+
changedPaths.push(name);
|
|
163
|
+
}
|
|
164
|
+
const scopeViolations = changedPaths.filter((name) => !allowedPaths.some((pattern) => pathMatchesPattern(name, pattern))
|
|
165
|
+
|| forbiddenPaths.some((pattern) => pathMatchesPattern(name, pattern)));
|
|
166
|
+
return { base, head: head.trim(), treeHash: sha256(JSON.stringify(files)), changedPaths, scopeViolations };
|
|
167
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { captureScheduledGoalTree } from "./scheduled-goal-evidence.js";
|
|
2
|
+
// ESRCH establishes absence only; permission errors and PID reuse never grant takeover.
|
|
3
|
+
function ownerMissing(pid) {
|
|
4
|
+
if (pid === undefined)
|
|
5
|
+
return true;
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return error.code === "ESRCH";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Read-only result projection. Reopening a result never authorizes another execution. */
|
|
15
|
+
export async function readScheduledGoalResult(goalId, goals) {
|
|
16
|
+
const goal = await goals.get(goalId);
|
|
17
|
+
const evidence = goal.evidence.at(-1);
|
|
18
|
+
let evidenceCurrent = false;
|
|
19
|
+
let treeReadError = false;
|
|
20
|
+
if (evidence) {
|
|
21
|
+
try {
|
|
22
|
+
const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
|
|
23
|
+
evidenceCurrent = evidence.intentId === goal.intents.at(-1)?.intentId && tree.treeHash === evidence.committedTree.treeHash &&
|
|
24
|
+
tree.treeHash === evidence.assessment.after.treeHash && tree.scopeViolations.length === 0;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
treeReadError = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const missingOwner = goal.intents.some((intent) => intent.state === "dispatching" && ownerMissing(intent.dispatchOwnerPid)) || Boolean(goal.assessmentAttempt && ownerMissing(goal.assessmentAttempt.ownerPid)) ||
|
|
31
|
+
goal.supervisions.some((entry) => ["requested", "preparing"].includes(entry.state) && ownerMissing(entry.ownerPid));
|
|
32
|
+
const uncertain = missingOwner || goal.state === "unknown" || goal.intents.some((intent) => intent.state === "unknown") ||
|
|
33
|
+
goal.supervisions.some((entry) => entry.state === "unknown") || Boolean(goal.assessmentAttempt && goal.acceptanceState === "pending");
|
|
34
|
+
const acceptance = evidence && !evidenceCurrent && ["accepted", "failed"].includes(goal.acceptanceState) ? "stale" : goal.acceptanceState;
|
|
35
|
+
return {
|
|
36
|
+
goalId, goalRevision: goal.goalRevision, stateVersion: goal.stateVersion,
|
|
37
|
+
state: goal.state, reasonCode: treeReadError ? "tree_unreadable" : acceptance === "stale" ? "evidence_drift" : goal.reasonCode,
|
|
38
|
+
acceptance, delivery: goal.deliveryState, evidenceCurrent,
|
|
39
|
+
budget: { limits: goal.frozen.budget, used: goal.used, tokenUsage: "unknown" },
|
|
40
|
+
supervision: goal.supervisions.map((entry) => ({ state: entry.state, taskRevision: entry.taskRevision, usageTokens: entry.usageTokens ?? null })),
|
|
41
|
+
execution: goal.intents.map((intent) => ({ taskRevision: intent.taskRevision, turnSeq: intent.turnSeq,
|
|
42
|
+
state: intent.state, operationId: intent.operationId, runId: intent.runId })),
|
|
43
|
+
evidence: goal.evidence.map((item) => ({ evidenceId: item.evidenceId, taskRevision: item.taskRevision,
|
|
44
|
+
treeHash: item.committedTree.treeHash, status: item.assessment.status, recordedAt: item.recordedAt,
|
|
45
|
+
criteria: item.assessment.criteria.map((ac) => ({ acId: ac.acId, status: ac.status, exitCode: ac.exitCode,
|
|
46
|
+
outputHash: ac.outputHash, evidenceRef: `${item.evidenceId}#${ac.acId}` })) })),
|
|
47
|
+
reconciliation: uncertain ? {
|
|
48
|
+
required: true,
|
|
49
|
+
reason: missingOwner ? "reservation_owner_missing" : goal.reasonCode ?? "execution_unknown",
|
|
50
|
+
instructions: "Inspect the associated operation/run and all descendant processes. Preserve the ledger and worktree. Do not delete ownership locks or automatically repeat an unknown action; an operator must reconcile execution before resources can be released.",
|
|
51
|
+
} : null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Resolve a hash-bound output reference without exposing dispatch/owner capabilities. */
|
|
55
|
+
export async function readScheduledGoalEvidence(goalId, evidenceId, acId, goals) {
|
|
56
|
+
const goal = await goals.get(goalId);
|
|
57
|
+
const evidence = goal.evidence.find((entry) => entry.evidenceId === evidenceId);
|
|
58
|
+
const criterion = evidence?.assessment.criteria.find((entry) => entry.acId === acId);
|
|
59
|
+
if (!evidence || !criterion)
|
|
60
|
+
throw new Error("scheduled goal evidence not found");
|
|
61
|
+
return { evidenceId, taskRevision: evidence.taskRevision, treeHash: evidence.committedTree.treeHash, ...criterion };
|
|
62
|
+
}
|