@tea-agent/loop-agent 0.43.0-next.18 → 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 +60 -2308
- package/README.md +7 -0
- package/dist/application/task-lifecycle/advance.js +38 -7
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +13 -9
- package/dist/commands/task-advance.js +32 -0
- package/dist/executors/dag-pi-executor.js +1615 -188
- 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/shared/backend-dogfood-preflight.js +47 -0
- package/dist/shared/dag-failure-category.js +3 -0
- package/dist/shared/operator/capabilities.js +180 -0
- package/dist/task/config-types.js +29 -0
- package/dist/task/contract/project.js +9 -0
- package/dist/task/contract/schema.js +17 -0
- package/dist/task/source-prepare/build-draft.js +60 -0
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
- 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 +86 -27
- package/dist/worker/console/chat/routes.js +19 -0
- 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/session-mode.js +7 -3
- 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/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-B_UDknhR.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6hU0drN.js → arc-eoQiMvuk.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DmVikdRK.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DydABCRg.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DrXKC2Jl.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-D2-qFxr7.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ChfeF41P.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Drwrz55x.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CAQGErL7.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DHO38w5U.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-EYIN0EOI.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CrFLKqkT.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMchffoo.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DaSvtKQa.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DcW8W9Qj.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-BUirrMEi.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXzO8iXR.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DAtgaxIn.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C1re9Noh.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-N8Kj08J9.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-D6BpSLs3.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C2Vlgnl4.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CMbqtVLS.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-A770Eb-0.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-LV2w_2pT.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-bwqf56ah.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BnVtHhZh.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGVToEqc.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DL2l7Vne.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-B_V4wvXs.js → index-Cdkvw_H6.js} +97 -97
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWIpubOx.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DjYBD8vv.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C0_mBaHb.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cy71zbX6.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-Dt3_w3Vn.js → linear-BDZ9riWi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DnNOlfEP.js → mermaid.core-7pKqYtpZ.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DAbTspwq.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-NNiM141p.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CN80Z1Do.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bxyfh0JU.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BlWnC79M.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BrEu4P_e.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bt70igBr.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C8DQR-r9.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DEMinJAT.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DDdDBzQc.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-G2wcBs3T.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-Dzn4g2gJ.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-5v2rP9oO.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-B4EOx7ew.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQ1v8eYP.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/timeline-merge.js +29 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
- package/dist/worker/console/workspace-context.js +11 -0
- package/dist/worker/observe/static/operator-chrome.js +3 -1
- 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/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/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 +11 -2
- 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 +182 -185
- package/dist/workflows/dag/lifecycle.js +7 -2
- package/dist/workflows/dag/node-execution.js +48 -13
- package/dist/workflows/dag/rerun-plan.js +10 -0
- package/dist/workflows/dag/rerun-task.js +77 -1
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/scheduler.js +2 -4
- package/dist/workflows/dag/types.js +23 -0
- package/docs/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 +6 -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-Drecd94a.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-8Udu0t8-.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-8Udu0t8-.js +0 -1
- package/dist/worker/console/static/assets/index-DcudonhZ.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CyCiS0Lt.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-U0lhi6N0.js +0 -8
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
|
@@ -37,6 +37,7 @@ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActive
|
|
|
37
37
|
import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
|
|
38
38
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
39
39
|
import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
|
|
40
|
+
import { buildPiModeLoopIsolationOptions } from "./pi-mode-loop-isolation.js";
|
|
40
41
|
import { applyProcessRetryOverride, } from "../../../shared/pi-retry-settings.js";
|
|
41
42
|
import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
|
|
42
43
|
import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
|
|
@@ -49,6 +50,8 @@ import { buildPersistentTerminalTools } from "./terminal-tools.js";
|
|
|
49
50
|
import { buildScmTools } from "./scm-tools.js";
|
|
50
51
|
import { buildGoalContinuationContext, } from "./goals.js";
|
|
51
52
|
import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
|
|
53
|
+
import { buildScheduledGoalTools } from "./pi-runtime/custom-tools/scheduled-goal-tools.js";
|
|
54
|
+
import { WorkflowToolExposure, configuredToolExposure, isScheduledBookingTurn, pinScheduledBookingTurnTools, } from "./pi-runtime/progressive-tools.js";
|
|
52
55
|
import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
|
|
53
56
|
import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
|
|
54
57
|
import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
|
|
@@ -80,13 +83,14 @@ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-b
|
|
|
80
83
|
*/
|
|
81
84
|
export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
82
85
|
"You are General Operator Chat.",
|
|
83
|
-
"
|
|
86
|
+
"For workflow requests use operator tools; discover missing capabilities via operatorCapabilities(target). Obey AGENTS.md. Deny apply_patch/full-tools/shell/coding-chat; probe with safe-read/safe-grep. Browser_*: visible Browser panel only (static http(s), no workspace edits/scripts).",
|
|
84
87
|
"No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
|
|
88
|
+
"Scheduled intent takes precedence over task content: prepareScheduledTask first; no curl/HTTP probes or tests; never perform the scheduled work now. Only a planned goalId means booked; never self-confirm.",
|
|
85
89
|
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
|
|
86
|
-
"Failure: terminal runSummary
|
|
87
|
-
"
|
|
88
|
-
"Continue until success, user stop, or no safe recovery remains
|
|
89
|
-
"
|
|
90
|
+
"Failure: dagReport(runId) → terminal runSummary. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). No dagResume or re-advance of run-failed tasks. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
|
|
91
|
+
"Each bounded round: summarize the previous round's error in one line, fix its root cause — not the symptom — then rerun the failed node with a fresh plan. Never resubmit an unchanged fix; after two rounds failing the same way, stop and report evidence and remaining routes.",
|
|
92
|
+
"Continue until success, user stop, or no safe recovery remains. Never create a new task for a transient failure or replace repair-pi with direct edits.",
|
|
93
|
+
"Report start, meaningful phase changes, failures, prolonged quiet and completion: what finished and what is next. Never narrate unchanged operationWait polls or heartbeats.",
|
|
90
94
|
].join("\n");
|
|
91
95
|
/**
|
|
92
96
|
* Built-in SDK tool names registered for every Chat session (ADR 0011).
|
|
@@ -387,6 +391,7 @@ export class ConsolePiRuntime {
|
|
|
387
391
|
/** Non-sensitive models.json metadata used to avoid reparsing unchanged config. */
|
|
388
392
|
modelConfigFiles = new Map();
|
|
389
393
|
sessionManagers = new Map();
|
|
394
|
+
toolExposures = new Map();
|
|
390
395
|
/** Per-session SDK service scope (resourceLoader / settingsManager) used by the live runtime snapshot. */
|
|
391
396
|
serviceScopes = new Map();
|
|
392
397
|
/** Snapshot revision: bumped on create / reopen / model / thinking / reload. */
|
|
@@ -489,7 +494,7 @@ export class ConsolePiRuntime {
|
|
|
489
494
|
this.sessionModes.set(sessionId, sessionMode);
|
|
490
495
|
return sessionMode;
|
|
491
496
|
}
|
|
492
|
-
async buildResourceLoaderOptions(base = {}, sessionId) {
|
|
497
|
+
async buildResourceLoaderOptions(base = {}, sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
|
|
493
498
|
const inheritedFactories = Array.isArray(base.extensionFactories)
|
|
494
499
|
? base.extensionFactories
|
|
495
500
|
: [];
|
|
@@ -520,6 +525,7 @@ export class ConsolePiRuntime {
|
|
|
520
525
|
pressureExtension,
|
|
521
526
|
wizardLocalHeadersExtension,
|
|
522
527
|
].filter((item) => Boolean(item));
|
|
528
|
+
const piMode = sessionMode === "pi";
|
|
523
529
|
return {
|
|
524
530
|
...base,
|
|
525
531
|
...(extraFactories.length > 0
|
|
@@ -527,18 +533,23 @@ export class ConsolePiRuntime {
|
|
|
527
533
|
extensionFactories: [...inheritedFactories, ...extraFactories],
|
|
528
534
|
}
|
|
529
535
|
: {}),
|
|
530
|
-
|
|
536
|
+
...buildPiModeLoopIsolationOptions({
|
|
537
|
+
enabled: piMode,
|
|
538
|
+
cwd: this.options.cwd,
|
|
531
539
|
// Synchronous override surface: use last-known prefs via a sync
|
|
532
540
|
// fail-closed path. Prefer async cache filled by ensurePrefs.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
541
|
+
readPreferences: () => this.cachedSkillPrefs,
|
|
542
|
+
skillsOverride: (current) => {
|
|
543
|
+
const prefs = this.cachedSkillPrefs ?? {
|
|
544
|
+
schemaVersion: 1,
|
|
545
|
+
skills: {},
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
skills: applySkillAutoInvocationPreferences(current.skills, prefs),
|
|
549
|
+
diagnostics: current.diagnostics,
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
542
553
|
};
|
|
543
554
|
}
|
|
544
555
|
/** Last successfully loaded skill prefs (sync override + snapshot projection). */
|
|
@@ -858,6 +869,9 @@ export class ConsolePiRuntime {
|
|
|
858
869
|
const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
|
|
859
870
|
if (aborted())
|
|
860
871
|
return undefined;
|
|
872
|
+
if (init.sessionMode === "workflow" && this.options.actionContext) {
|
|
873
|
+
this.toolExposures.set(init.sessionId, new WorkflowToolExposure(init.sessionManager, this.options.toolExposure ?? configuredToolExposure(process.env.CONSOLE_CHAT_TOOL_EXPOSURE)));
|
|
874
|
+
}
|
|
861
875
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
862
876
|
// Capture before services parse models.json. A concurrent edit can therefore
|
|
863
877
|
// cause at most one harmless extra refresh, never a missed next-turn refresh.
|
|
@@ -871,7 +885,7 @@ export class ConsolePiRuntime {
|
|
|
871
885
|
const { services } = await this.bindings.createServices({
|
|
872
886
|
cwd: this.options.cwd,
|
|
873
887
|
agentDir,
|
|
874
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
|
|
888
|
+
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId, init.sessionMode),
|
|
875
889
|
});
|
|
876
890
|
if (aborted())
|
|
877
891
|
return undefined;
|
|
@@ -913,8 +927,19 @@ export class ConsolePiRuntime {
|
|
|
913
927
|
}
|
|
914
928
|
return undefined;
|
|
915
929
|
}
|
|
916
|
-
|
|
917
|
-
|
|
930
|
+
try {
|
|
931
|
+
await bindSessionExtensions(session);
|
|
932
|
+
this.pinSessionTools(init.sessionId, session);
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
try {
|
|
936
|
+
session.dispose();
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
this.toolExposures.delete(init.sessionId);
|
|
940
|
+
}
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
918
943
|
this.applySessionRecoveryOverrides(init.sessionId, services, session);
|
|
919
944
|
return {
|
|
920
945
|
session,
|
|
@@ -963,6 +988,7 @@ export class ConsolePiRuntime {
|
|
|
963
988
|
}
|
|
964
989
|
this.sessions.delete(sessionId);
|
|
965
990
|
this.sessionManagers.delete(sessionId);
|
|
991
|
+
this.toolExposures.delete(sessionId);
|
|
966
992
|
this.modelRuntimes.delete(sessionId);
|
|
967
993
|
this.modelConfigFiles.delete(sessionId);
|
|
968
994
|
this.sessionModels.delete(sessionId);
|
|
@@ -972,6 +998,20 @@ export class ConsolePiRuntime {
|
|
|
972
998
|
this.thinkingSelections.delete(sessionId);
|
|
973
999
|
}
|
|
974
1000
|
}
|
|
1001
|
+
pinSessionTools(sessionId, session, interview = false) {
|
|
1002
|
+
const exposure = this.toolExposures.get(sessionId);
|
|
1003
|
+
if (exposure) {
|
|
1004
|
+
exposure.pin(session, interview);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (interview) {
|
|
1008
|
+
session.setActiveToolsByName(filterActiveInterviewTools(chatActiveToolBaseline(this.sessionModeOf(sessionId))).allowed);
|
|
1009
|
+
}
|
|
1010
|
+
else
|
|
1011
|
+
pinOperatorChatTools(session, this.sessionModeOf(sessionId));
|
|
1012
|
+
}
|
|
1013
|
+
/** Read-only rollout diagnostics; no model parameters or host identities. */
|
|
1014
|
+
getToolExposure(sessionId) { return this.toolExposures.get(sessionId)?.snapshot; }
|
|
975
1015
|
/** Cached session-less custom tool definitions (operator + explore). */
|
|
976
1016
|
async loadCustomTools(sessionId) {
|
|
977
1017
|
const sessionMode = this.sessionModeOf(sessionId);
|
|
@@ -982,6 +1022,8 @@ export class ConsolePiRuntime {
|
|
|
982
1022
|
: Promise.resolve([]),
|
|
983
1023
|
this.loadExploreCustomTools(),
|
|
984
1024
|
]);
|
|
1025
|
+
const scheduledGoalTools = loadWorkflowTools && this.options.actionContext
|
|
1026
|
+
? await buildScheduledGoalTools({ sessionId, actionContext: this.options.actionContext }) : [];
|
|
985
1027
|
const browserTools = await buildBrowserTools({
|
|
986
1028
|
automation: this.browserAutomation,
|
|
987
1029
|
});
|
|
@@ -1035,8 +1077,14 @@ export class ConsolePiRuntime {
|
|
|
1035
1077
|
process.stderr.write(`[console] chat Agent tool build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1036
1078
|
return undefined;
|
|
1037
1079
|
});
|
|
1080
|
+
const exposure = this.toolExposures.get(sessionId);
|
|
1081
|
+
const fullCapabilities = operatorCustomTools.find(tool => tool.name === "operatorCapabilities");
|
|
1082
|
+
// Always session-bound: a branch can restore progressive after full fallback.
|
|
1083
|
+
const discovery = exposure?.buildTool(fullCapabilities);
|
|
1038
1084
|
return [
|
|
1039
|
-
...operatorCustomTools,
|
|
1085
|
+
...operatorCustomTools.filter(tool => !discovery || tool.name !== "operatorCapabilities"),
|
|
1086
|
+
...(discovery ? [discovery] : []),
|
|
1087
|
+
...scheduledGoalTools,
|
|
1040
1088
|
...exploreCustomTools,
|
|
1041
1089
|
...(questionTool ? [questionTool] : []),
|
|
1042
1090
|
...(todoTool ? [todoTool] : []),
|
|
@@ -1126,6 +1174,7 @@ export class ConsolePiRuntime {
|
|
|
1126
1174
|
const entry = input.summary
|
|
1127
1175
|
? manager.branchWithSummary?.(input.branchFromEntryId, input.summary)
|
|
1128
1176
|
: (manager.branch(input.branchFromEntryId), undefined);
|
|
1177
|
+
this.pinSessionTools(input.sessionId, session);
|
|
1129
1178
|
return {
|
|
1130
1179
|
branchedFromEntryId: input.branchFromEntryId,
|
|
1131
1180
|
...(entry?.id ? { branchSummaryEntryId: entry.id } : {}),
|
|
@@ -1137,6 +1186,9 @@ export class ConsolePiRuntime {
|
|
|
1137
1186
|
throw new Error(`chat session not active: ${sessionId}`);
|
|
1138
1187
|
manager.branch(this.mainlineLeaves.get(sessionId));
|
|
1139
1188
|
this.mainlineLeaves.delete(sessionId);
|
|
1189
|
+
const session = this.sessions.get(sessionId);
|
|
1190
|
+
if (session)
|
|
1191
|
+
this.pinSessionTools(sessionId, session);
|
|
1140
1192
|
}
|
|
1141
1193
|
getBranchContext(sessionId) {
|
|
1142
1194
|
const manager = this.sessionManagers.get(sessionId);
|
|
@@ -1396,7 +1448,7 @@ export class ConsolePiRuntime {
|
|
|
1396
1448
|
if (services)
|
|
1397
1449
|
this.applySessionRecoveryOverrides(sessionId, services, session);
|
|
1398
1450
|
await this.refreshModelConfiguration(sessionId, "force");
|
|
1399
|
-
|
|
1451
|
+
this.pinSessionTools(sessionId, session);
|
|
1400
1452
|
}
|
|
1401
1453
|
catch (error) {
|
|
1402
1454
|
return {
|
|
@@ -1538,7 +1590,14 @@ export class ConsolePiRuntime {
|
|
|
1538
1590
|
extensions: inventories.extensions,
|
|
1539
1591
|
packages: inventories.packages,
|
|
1540
1592
|
mcp: inventories.mcp,
|
|
1541
|
-
diagnostics:
|
|
1593
|
+
diagnostics: [
|
|
1594
|
+
...inventories.diagnostics,
|
|
1595
|
+
...(this.toolExposures.has(sessionId) ? [{
|
|
1596
|
+
type: "info",
|
|
1597
|
+
code: "WORKFLOW_TOOL_EXPOSURE",
|
|
1598
|
+
message: JSON.stringify(this.toolExposures.get(sessionId).snapshot),
|
|
1599
|
+
}] : []),
|
|
1600
|
+
],
|
|
1542
1601
|
hasBash: true,
|
|
1543
1602
|
});
|
|
1544
1603
|
}
|
|
@@ -1855,11 +1914,10 @@ export class ConsolePiRuntime {
|
|
|
1855
1914
|
// explicitly restores the normal Operator Chat set (baseline + extension
|
|
1856
1915
|
// tools, ADR 0012).
|
|
1857
1916
|
const sessionMode = this.sessionModeOf(sessionId);
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
session.setActiveToolsByName(activeTools);
|
|
1917
|
+
this.pinSessionTools(sessionId, session, options?.mode === "requirement-interview" && sessionMode === "workflow");
|
|
1918
|
+
if (sessionMode === "workflow" && isScheduledBookingTurn(text)) {
|
|
1919
|
+
pinScheduledBookingTurnTools(session);
|
|
1920
|
+
}
|
|
1863
1921
|
// Final pre-prompt fence: abort after tool pin must still skip session.prompt.
|
|
1864
1922
|
if (options?.signal?.aborted) {
|
|
1865
1923
|
return;
|
|
@@ -2192,6 +2250,7 @@ export class ConsolePiRuntime {
|
|
|
2192
2250
|
}
|
|
2193
2251
|
this.sessions.delete(sessionId);
|
|
2194
2252
|
this.sessionManagers.delete(sessionId);
|
|
2253
|
+
this.toolExposures.delete(sessionId);
|
|
2195
2254
|
this.mainlineLeaves.delete(sessionId);
|
|
2196
2255
|
this.modelRuntimes.delete(sessionId);
|
|
2197
2256
|
this.modelConfigFiles.delete(sessionId);
|
|
@@ -134,6 +134,16 @@ export function projectStoredMessagesWithEventOrder(messages, events) {
|
|
|
134
134
|
if (event.kind !== expectedKind)
|
|
135
135
|
continue;
|
|
136
136
|
const data = event.data;
|
|
137
|
+
// Exact alignment wins: since 2026-09-13 message_end frames carry the
|
|
138
|
+
// durable assistant message id, so restore matches by id, never by
|
|
139
|
+
// text (duplicate assistant texts would mis-align within a turn).
|
|
140
|
+
if (typeof data.messageId === "string" && data.messageId) {
|
|
141
|
+
if (data.messageId === message.id) {
|
|
142
|
+
assistantCursor = index + 1;
|
|
143
|
+
return event;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
137
147
|
const eventText = typeof data.text === "string"
|
|
138
148
|
? data.text
|
|
139
149
|
: typeof data.message === "string"
|
|
@@ -271,6 +281,15 @@ function turnEventToStorePartial(event) {
|
|
|
271
281
|
text: String(event.text ?? ""),
|
|
272
282
|
...(content ? { content } : {}),
|
|
273
283
|
...(timing ? { timing } : {}),
|
|
284
|
+
// Durable assistant message id (2026-09-13): the session
|
|
285
|
+
// store persists message_end BEFORE framing, so a reconnecting
|
|
286
|
+
// client and the restore projection can align on this id
|
|
287
|
+
// instead of matching text within a time window.
|
|
288
|
+
...(typeof event.messageId ===
|
|
289
|
+
"string" &&
|
|
290
|
+
event.messageId
|
|
291
|
+
? { messageId: event.messageId }
|
|
292
|
+
: {}),
|
|
274
293
|
},
|
|
275
294
|
};
|
|
276
295
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { prepareScheduledGoalDraft } from "../../scheduler/scheduled-goal-dispatch.js";
|
|
3
|
+
import { scheduledGoalDraftSchema } from "../../scheduler/scheduled-goal-types.js";
|
|
4
|
+
import { scheduledGoalHash } from "../../scheduler/scheduled-goal-store.js";
|
|
5
|
+
import { readScheduledGoalResult } from "../../scheduler/scheduled-goal-recovery.js";
|
|
6
|
+
export const scheduledGoalBookingSchema = scheduledGoalDraftSchema.omit({ prepared: true, controllerRef: true }).extend({ chatSessionRef: z.string().regex(/^[a-zA-Z0-9_-]+$/) });
|
|
7
|
+
export const scheduledGoalConfirmationSchema = z.object({
|
|
8
|
+
goalId: z.string().min(1), stateVersion: z.number().int().positive(),
|
|
9
|
+
envelopeHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
10
|
+
}).strict();
|
|
11
|
+
/** Only non-capability fields are returned to Chat/browser. */
|
|
12
|
+
export function projectScheduledGoalBooking(goal) {
|
|
13
|
+
const { prepared: _prepared, controllerRef, ...scope } = goal.frozen;
|
|
14
|
+
return {
|
|
15
|
+
goalId: goal.goalId, stateVersion: goal.stateVersion, goalRevision: goal.goalRevision,
|
|
16
|
+
envelopeHash: goal.envelopeHash, state: goal.state, scope, controllerRef,
|
|
17
|
+
confirmationRequired: goal.state === "planned",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Preparation is not authorization, even when the clock is already due. */
|
|
21
|
+
export async function prepareScheduledGoalBooking(input, deps, resolveFinalTime) {
|
|
22
|
+
const parsed = scheduledGoalBookingSchema.parse(input);
|
|
23
|
+
const session = await deps.sessions.get(parsed.chatSessionRef);
|
|
24
|
+
if (!session || session.state !== "active")
|
|
25
|
+
throw new Error("booking requires an active Chat session");
|
|
26
|
+
const admitted = await prepareScheduledGoalDraft(parsed, deps.client);
|
|
27
|
+
// High-level relative time starts after slow strict admission, not before it.
|
|
28
|
+
const frozen = resolveFinalTime ? scheduledGoalDraftSchema.parse({ ...admitted, time: resolveFinalTime() }) : admitted;
|
|
29
|
+
const existing = (await deps.goals.list()).find((goal) => goal.frozen.managedTaskId === frozen.managedTaskId && goal.state === "planned" &&
|
|
30
|
+
scheduledGoalHash(goal.frozen) === scheduledGoalHash(frozen));
|
|
31
|
+
const goal = existing ?? await deps.goals.createDraft(frozen);
|
|
32
|
+
return projectScheduledGoalBooking(goal);
|
|
33
|
+
}
|
|
34
|
+
export async function confirmScheduledGoalBooking(input, deps, human) {
|
|
35
|
+
// Provenance is supplied by the authenticated route, never parsed from model input.
|
|
36
|
+
if (human.source !== "human-ui" || !human.requestId?.trim())
|
|
37
|
+
throw new Error("human confirmation required");
|
|
38
|
+
const parsed = scheduledGoalConfirmationSchema.parse(input);
|
|
39
|
+
const goal = await deps.goals.get(parsed.goalId);
|
|
40
|
+
if (goal.approval?.approvalRef === human.requestId && goal.approval.envelopeHash === parsed.envelopeHash &&
|
|
41
|
+
goal.approval.approvedVersion === parsed.stateVersion)
|
|
42
|
+
return projectScheduledGoalBooking(goal);
|
|
43
|
+
const session = await deps.sessions.get(goal.frozen.chatSessionRef);
|
|
44
|
+
if (!session || session.state !== "active")
|
|
45
|
+
throw new Error("booking requires an active Chat session");
|
|
46
|
+
if (goal.envelopeHash !== parsed.envelopeHash || goal.stateVersion !== parsed.stateVersion || goal.state !== "planned")
|
|
47
|
+
throw new Error("booking confirmation binding mismatch");
|
|
48
|
+
// Revalidate controller, strict gate, Task and scope immediately before approval.
|
|
49
|
+
const { prepared: _prepared, controllerRef: _controller, ...scope } = goal.frozen;
|
|
50
|
+
const fresh = await prepareScheduledGoalDraft(scope, deps.client);
|
|
51
|
+
if (scheduledGoalHash(fresh) !== scheduledGoalHash(goal.frozen))
|
|
52
|
+
throw new Error("booking preparation drift");
|
|
53
|
+
const approved = await deps.goals.approve(goal.goalId, parsed.stateVersion, parsed.envelopeHash, human.requestId);
|
|
54
|
+
return projectScheduledGoalBooking(approved);
|
|
55
|
+
}
|
|
56
|
+
export async function getScheduledGoalBooking(goalId, goals) {
|
|
57
|
+
const goal = await goals.get(goalId);
|
|
58
|
+
return { ...projectScheduledGoalBooking(goal), result: await readScheduledGoalResult(goalId, goals) };
|
|
59
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { scheduledGoalDeliveryKey } from "../../scheduler/scheduled-goal-store.js";
|
|
2
|
+
/** The durable Chat record deduplicates a retry after ledger acknowledgement is lost.
|
|
3
|
+
* A missing Chat never gets recreated. The goal remains available through its ledger.
|
|
4
|
+
*/
|
|
5
|
+
export async function deliverScheduledGoalResult(goal, goals, sessions) {
|
|
6
|
+
const key = scheduledGoalDeliveryKey(goal);
|
|
7
|
+
if (goal.deliveryKey === key && goal.deliveryState === "delivered")
|
|
8
|
+
return;
|
|
9
|
+
const evidence = goal.evidence.at(-1);
|
|
10
|
+
const states = {
|
|
11
|
+
accepted: "已验收", cancelled: "已取消", blocked: "已停止", budget_exhausted: "预算用尽", unknown: "需要核对",
|
|
12
|
+
failed: "未通过", pending: "待处理", stale: "证据已失效", succeeded: "通过",
|
|
13
|
+
};
|
|
14
|
+
const label = (value) => states[value] ?? value;
|
|
15
|
+
const text = [
|
|
16
|
+
`预约目标:${goal.frozen.originalGoal}`,
|
|
17
|
+
`目标状态:${label(goal.state)};验收:${label(goal.acceptanceState)}。已执行 ${goal.intents.length} 轮。`,
|
|
18
|
+
...(goal.reasonCode ? ["停止原因与需要核对的运行记录,可在本会话的预约面板查看。"] : []),
|
|
19
|
+
...(evidence ? evidence.assessment.criteria.map((ac) => {
|
|
20
|
+
const criterion = goal.frozen.acceptance.criteria.find((item) => item.acId === ac.acId);
|
|
21
|
+
return `${ac.acId} · ${criterion?.description ?? ac.acId}:${label(ac.status)}${ac.exitCode !== 0 ? `(退出码 ${ac.exitCode ?? "未知"})` : ""}。`;
|
|
22
|
+
}) : []),
|
|
23
|
+
`预约编号:${goal.goalId}。证据和历史保留在预约面板;删除会话不会删除运行事实。`,
|
|
24
|
+
].join("\n\n");
|
|
25
|
+
const delivered = await sessions.appendScheduledGoalResult(goal.frozen.chatSessionRef, key, text);
|
|
26
|
+
await goals.recordDelivery(goal.goalId, key, delivered ? "delivered" : "unavailable");
|
|
27
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { getTaskPaths } from "../../../task/runtime.js";
|
|
8
|
+
import { scheduledGoalDraftSchema, scheduledGoalTimeSchema } from "../../scheduler/scheduled-goal-types.js";
|
|
9
|
+
import { resolveScheduledGoalTime } from "../../scheduler/scheduled-goal-time.js";
|
|
10
|
+
import { ScheduledGoalStore, scheduledGoalHash } from "../../scheduler/scheduled-goal-store.js";
|
|
11
|
+
import { captureScheduledGoalTree } from "../../scheduler/scheduled-goal-evidence.js";
|
|
12
|
+
import { getScheduledGoalBooking, prepareScheduledGoalBooking } from "./scheduled-goal-booking.js";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
const protectedPaths = [".harness/**", "node_modules/**", ".git/**"];
|
|
15
|
+
const relativePath = z.string().trim().min(1).max(500).refine((value) => !path.isAbsolute(value) && !/^[A-Za-z]:/.test(value) && !value.includes("\\") &&
|
|
16
|
+
!value.split("/").some((part) => part === ".." || part === "." || part === "") && !value.startsWith("-"), "use a relative repository path without traversal");
|
|
17
|
+
const scheduleSchema = z.object({
|
|
18
|
+
delayMinutes: z.number().positive().max(525600).optional().describe("Delay AFTER preparation finishes; use for '3 minutes later'. The confirmation card shows the resulting exact time."),
|
|
19
|
+
localAt: z.string().regex(/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$/).optional().describe("Explicit local date/time YYYY-MM-DD HH:mm; never silently move a past date."),
|
|
20
|
+
timezone: z.string().min(1).optional().describe("IANA timezone, e.g. Asia/Shanghai for Beijing. Defaults to the Console host timezone."),
|
|
21
|
+
fold: z.enum(["earlier", "later"]).optional(),
|
|
22
|
+
allowedHours: z.array(z.string()).optional().describe("Local allowed windows; default [] means all day."),
|
|
23
|
+
startGraceMinutes: z.number().positive().max(1440).optional(),
|
|
24
|
+
maxDurationMinutes: z.number().positive().max(1440).optional(),
|
|
25
|
+
}).strict().refine((value) => (value.delayMinutes !== undefined) !== (value.localAt !== undefined), "choose exactly one of delayMinutes or localAt");
|
|
26
|
+
/** The model supplies business meaning, never runtime identity or authorization. */
|
|
27
|
+
export const scheduledTaskRequestSchema = z.object({
|
|
28
|
+
originalGoal: z.string().trim().min(1).max(24000).describe("Faithfully preserve the complete user goal and constraints. Do not add unrequested failure cases, mocks, filenames or acceptance obligations; preserve real-service testing requirements."),
|
|
29
|
+
workflow: scheduledGoalDraftSchema.shape.workflow,
|
|
30
|
+
allowedPaths: z.array(relativePath).min(1).max(100).describe("Business outputs in the CURRENT project directory. For a read-only inspection, allow only a report such as scheduled-report.md, never .harness/."),
|
|
31
|
+
forbiddenPaths: z.array(relativePath).max(100).optional(),
|
|
32
|
+
acceptance: scheduledGoalDraftSchema.shape.acceptance.describe("Use minimal independent executable checks with AC-1, AC-2 IDs. Each command must exit nonzero on failure. For backend-test, acceptance must only inspect the finished run and must never invoke pytest. Prefer an existing read-only verification command or test -s report.md. Do not invent extra requirements, language-specific grep checks, shell pipelines, command substitution, redirects or compound shell statements. The host already enforces write scope."),
|
|
33
|
+
budget: scheduledGoalDraftSchema.shape.budget.optional().describe("Omit for ordinary tasks: defaults to one DAG run, one goal attempt and one supervisor call. maxTurns counts goal attempts, NOT model tool calls; maxSupervisorCalls does NOT count polling. Always displayed before confirmation."),
|
|
34
|
+
schedule: scheduleSchema,
|
|
35
|
+
}).strict();
|
|
36
|
+
export function resolveScheduledTaskTime(input, now) {
|
|
37
|
+
const schedule = scheduleSchema.parse(input);
|
|
38
|
+
const timezone = schedule.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
39
|
+
const executeAtUtc = schedule.localAt
|
|
40
|
+
? resolveScheduledGoalTime({ localAt: schedule.localAt, timezone, fold: schedule.fold })
|
|
41
|
+
: new Date(now.getTime() + schedule.delayMinutes * 60000).toISOString();
|
|
42
|
+
if (Date.parse(executeAtUtc) <= now.getTime())
|
|
43
|
+
throw new Error("requested time is in the past; provide a new future time (不会自动顺延)");
|
|
44
|
+
const latestStart = Date.parse(executeAtUtc) + (schedule.startGraceMinutes ?? 15) * 60000;
|
|
45
|
+
const stop = latestStart + (schedule.maxDurationMinutes ?? 60) * 60000;
|
|
46
|
+
return scheduledGoalTimeSchema.parse({ executeAtUtc, latestStartUtc: new Date(latestStart).toISOString(),
|
|
47
|
+
stopAtUtc: new Date(stop).toISOString(), expiresAt: new Date(stop).toISOString(), timezone,
|
|
48
|
+
allowedHours: schedule.allowedHours ?? [], misfirePolicy: "run-immediately" });
|
|
49
|
+
}
|
|
50
|
+
const git = async (cwd, ...args) => (await execFileAsync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 30000, maxBuffer: 4 * 1024 * 1024 })).stdout.trim();
|
|
51
|
+
const sameSet = (a, b) => JSON.stringify([...new Set(a)].sort()) === JSON.stringify([...new Set(b)].sort());
|
|
52
|
+
function assertScheduledAcceptanceDoesNotRepeatBackendTests(request) {
|
|
53
|
+
if (request.workflow !== "backend-test")
|
|
54
|
+
return;
|
|
55
|
+
const repeated = request.acceptance.criteria.find((criterion) => /(?:^|\s)(?:python(?:3)?\s+-m\s+)?pytest\b/i.test(criterion.command));
|
|
56
|
+
if (repeated)
|
|
57
|
+
throw new Error(`backend-test acceptance must not invoke pytest; ${repeated.acId} must inspect the completed run instead`);
|
|
58
|
+
}
|
|
59
|
+
/** Synchronous PREPARATION only. CLI calls are bounded and NEVER approve a DAG.
|
|
60
|
+
* A durable exclusive request marker prevents retries from creating duplicate Tasks
|
|
61
|
+
* or overwriting imported source. Unknown interrupted preparation is not reclaimed.
|
|
62
|
+
*/
|
|
63
|
+
export async function prepareScheduledTask(input, deps) {
|
|
64
|
+
const request = scheduledTaskRequestSchema.parse(input);
|
|
65
|
+
assertScheduledAcceptanceDoesNotRepeatBackendTests(request);
|
|
66
|
+
for (const ref of request.allowedPaths) {
|
|
67
|
+
if (/^(?:\.harness|\.git|node_modules)(?:\/|$)/.test(ref) || ref.includes("*") && !/^[\w.-]+\//.test(ref))
|
|
68
|
+
throw new Error(`protected or unbounded output path ${ref}; use an ordinary bounded report/output path, never runtime directories`);
|
|
69
|
+
}
|
|
70
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(deps.sessionId) || (await deps.sessions.get(deps.sessionId))?.state !== "active")
|
|
71
|
+
throw new Error("preparation requires an active bound Chat session");
|
|
72
|
+
const identity = deps.client.getIdentity();
|
|
73
|
+
if (!identity)
|
|
74
|
+
throw new Error("preparation requires a pinned controller");
|
|
75
|
+
const repoRoot = await realpath(deps.repoRoot);
|
|
76
|
+
const now = deps.now ?? (() => new Date());
|
|
77
|
+
const key = scheduledGoalHash({ repoRoot, sessionId: deps.sessionId, request });
|
|
78
|
+
// Keep generated paths compact: runtimes/extensions may encode the whole cwd
|
|
79
|
+
// into one filename. The full hash below still verifies receipt identity.
|
|
80
|
+
const directory = path.join(deps.preparationRoot, key.slice(0, 24));
|
|
81
|
+
const recordPath = path.join(directory, "preparation.json");
|
|
82
|
+
const goals = new ScheduledGoalStore(repoRoot);
|
|
83
|
+
const existing = await readFile(recordPath, "utf8").then((text) => JSON.parse(text)).catch((error) => {
|
|
84
|
+
if (error.code === "ENOENT")
|
|
85
|
+
return undefined;
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
if (existing) {
|
|
89
|
+
if (existing.version !== 1 || existing.requestHash !== key)
|
|
90
|
+
throw new Error("preparation record binding mismatch");
|
|
91
|
+
if (existing.goalId)
|
|
92
|
+
return getScheduledGoalBooking(existing.goalId, goals);
|
|
93
|
+
// Covers a crash after the goal ledger commit but before the local receipt.
|
|
94
|
+
const committed = (await goals.list()).find((goal) => goal.frozen.managedTaskId === existing.taskId && goal.frozen.chatSessionRef === deps.sessionId);
|
|
95
|
+
if (committed)
|
|
96
|
+
return getScheduledGoalBooking(committed.goalId, goals);
|
|
97
|
+
throw new Error(`previous preparation ${existing.stage}: ${existing.error ?? "still running or interrupted; do not duplicate or edit its artifacts"}; evidence: ${recordPath}`);
|
|
98
|
+
}
|
|
99
|
+
// Replays return their durable result even after the requested time has passed.
|
|
100
|
+
// New requests validate time before creating artifacts; relative time is also
|
|
101
|
+
// resolved after preparation so preparation cannot consume the delay.
|
|
102
|
+
resolveScheduledTaskTime(request.schedule, now());
|
|
103
|
+
// Check before creating a Task or receipt; never clean or reset the user's workspace.
|
|
104
|
+
const base = await git(repoRoot, "rev-parse", "HEAD");
|
|
105
|
+
const initial = await captureScheduledGoalTree(repoRoot, base, request.allowedPaths, protectedPaths);
|
|
106
|
+
if (initial.changedPaths.length)
|
|
107
|
+
throw new Error(`current workspace has uncommitted changes: ${initial.changedPaths.slice(0, 10).join(", ")}. Resolve them before booking; do not automatically commit, stash or reset user changes.`);
|
|
108
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
const lockPath = path.join(directory, "preparing.lock");
|
|
110
|
+
const lock = await open(lockPath, "wx", 0o600).catch((error) => {
|
|
111
|
+
if (error.code === "EEXIST")
|
|
112
|
+
throw new Error("preparation already running; do not duplicate or take over");
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
const record = { version: 1, requestHash: key, stage: "workspace", taskId: `scheduled-${key.slice(0, 24)}`, worktree: repoRoot };
|
|
116
|
+
const persist = async () => {
|
|
117
|
+
const temporary = `${recordPath}.${randomUUID()}.tmp`;
|
|
118
|
+
await writeFile(temporary, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
119
|
+
await rename(temporary, recordPath);
|
|
120
|
+
};
|
|
121
|
+
let started = false;
|
|
122
|
+
try {
|
|
123
|
+
// Recheck after acquiring the lock: another request may have finished between
|
|
124
|
+
// our initial read and lock acquisition.
|
|
125
|
+
const raced = await readFile(recordPath, "utf8").catch((e) => { if (e.code === "ENOENT")
|
|
126
|
+
return undefined; throw e; });
|
|
127
|
+
if (raced)
|
|
128
|
+
throw new Error("preparation changed concurrently; retry reading the existing request");
|
|
129
|
+
started = true;
|
|
130
|
+
await persist();
|
|
131
|
+
const forbiddenPaths = [...new Set([...protectedPaths, ...(request.forbiddenPaths ?? [])])];
|
|
132
|
+
const prdPath = path.join(directory, "request.md");
|
|
133
|
+
const prd = ["# Scheduled task", "## Goal", request.originalGoal,
|
|
134
|
+
"## Execution context", `Execution and all outputs: current project directory ${record.worktree}.`,
|
|
135
|
+
"Inspect this project directly. Write only the authorized outputs here; for read-only inspection, write only the requested report. Implementation changes remain in this directory for the user to review. Do not create another worktree, copy outputs elsewhere, or reset/stash existing changes. The confirmed Git base must remain unchanged until execution finishes.",
|
|
136
|
+
"## Acceptance criteria", ...request.acceptance.criteria.map((ac) => `- ${ac.acId}: ${ac.description}\n Verification: ${ac.command}`),
|
|
137
|
+
"## Boundaries", `Workflow: ${request.workflow}; policy: ${request.acceptance.policy}.`,
|
|
138
|
+
`Allowed outputs: ${JSON.stringify(request.allowedPaths)}. Forbidden outputs: ${JSON.stringify(forbiddenPaths)}.`,
|
|
139
|
+
"Never modify imported requirements, .harness runtime, dependencies, Git history or configuration. No push, merge or publication. The host delivers the final result to the original Chat; do not implement a second delivery/scheduling mechanism.",
|
|
140
|
+
"## Verification", ...request.acceptance.criteria.map((ac) => ac.command), ""].join("\n\n");
|
|
141
|
+
await writeFile(prdPath, prd, { flag: "wx", mode: 0o600 });
|
|
142
|
+
const boundary = request.allowedPaths.flatMap((ref) => ["--allowed-path", ref])
|
|
143
|
+
.concat(forbiddenPaths.flatMap((ref) => ["--forbidden-path", ref]))
|
|
144
|
+
.concat(request.acceptance.criteria.flatMap((ac) => ["--verify", `${ac.acId}:${ac.command}`]));
|
|
145
|
+
for (let step = 0; step < 2; step++) {
|
|
146
|
+
record.stage = step === 0 ? "task-import" : "task-prepare";
|
|
147
|
+
await persist();
|
|
148
|
+
const args = ["task", "advance", record.taskId, ...(step === 0 ? ["Scheduled task", "--prd", prdPath] : []),
|
|
149
|
+
"--task-kind", request.workflow, ...boundary, "--json"];
|
|
150
|
+
const result = await deps.client.run(args, { cwd: record.worktree, artifactName: `${record.taskId}-${step}`, expectJson: true, timeoutMs: 300000 });
|
|
151
|
+
if (!result.ok || result.exitCode !== 0 || result.timedOut) {
|
|
152
|
+
const detail = JSON.stringify(result.json ?? result.stderr ?? "no CLI evidence").slice(0, 6000);
|
|
153
|
+
throw new Error(`${record.stage} failed: ${detail}. Correct the request and re-import through this tool; never edit source/references or runtime files.`);
|
|
154
|
+
}
|
|
155
|
+
const envelope = result.json;
|
|
156
|
+
if (envelope?.ok !== true)
|
|
157
|
+
throw new Error(`${record.stage}: missing successful CLI envelope`);
|
|
158
|
+
if (envelope.result?.lifecycleState === "awaiting-write-set-approval")
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
record.stage = "booking";
|
|
162
|
+
await persist();
|
|
163
|
+
// Freeze actual strict-prepared path ordering/default restrictions. Never adopt
|
|
164
|
+
// a broader allowed set or drop a caller restriction to make admission pass.
|
|
165
|
+
const config = z.object({ taskKind: z.string(), allowedPaths: z.array(z.string()), forbiddenPaths: z.array(z.string()) }).parse(JSON.parse(await readFile(getTaskPaths(record.worktree, record.taskId).taskConfigPath, "utf8")));
|
|
166
|
+
if (config.taskKind !== request.workflow || !sameSet(config.allowedPaths, request.allowedPaths) || forbiddenPaths.some((ref) => !config.forbiddenPaths.includes(ref)))
|
|
167
|
+
throw new Error("prepared task scope differs from request; refusing to widen scope");
|
|
168
|
+
if (deps.client.getIdentity()?.packageFingerprint.value !== identity.packageFingerprint.value)
|
|
169
|
+
throw new Error("controller changed during preparation");
|
|
170
|
+
const booking = await prepareScheduledGoalBooking({ managedTaskId: record.taskId, originalGoal: request.originalGoal,
|
|
171
|
+
chatSessionRef: deps.sessionId, workflow: request.workflow, worktreeRef: record.worktree, frozenBase: base,
|
|
172
|
+
allowedPaths: config.allowedPaths, forbiddenPaths: config.forbiddenPaths, acceptance: request.acceptance,
|
|
173
|
+
budget: request.budget ?? { maxTurns: 1, maxDagRuns: 1, maxSupervisorCalls: 1 }, time: resolveScheduledTaskTime(request.schedule, now()),
|
|
174
|
+
}, { goals, client: deps.client, sessions: deps.sessions }, () => resolveScheduledTaskTime(request.schedule, now()));
|
|
175
|
+
record.goalId = booking.goalId;
|
|
176
|
+
record.stage = "planned";
|
|
177
|
+
await persist();
|
|
178
|
+
return booking;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
182
|
+
if (started)
|
|
183
|
+
await persist();
|
|
184
|
+
throw new Error(`Scheduled task preparation stopped at ${record.stage}: ${record.error}`);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await lock.close();
|
|
188
|
+
await unlink(lockPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -135,8 +135,10 @@ export function chatActiveToolBaseline(sessionMode) {
|
|
|
135
135
|
* not re-add operator actions via the extras pass (Goal tools are shared and
|
|
136
136
|
* part of the Pi baseline since 2026-09-10).
|
|
137
137
|
*/
|
|
138
|
-
export function computeChatActiveToolNames(session, sessionMode) {
|
|
139
|
-
const
|
|
138
|
+
export function computeChatActiveToolNames(session, sessionMode, exposedWorkflowTools) {
|
|
139
|
+
const exposure = sessionMode === "workflow" && exposedWorkflowTools
|
|
140
|
+
? new Set(exposedWorkflowTools.map(name => name.toLowerCase())) : undefined;
|
|
141
|
+
const baseline = chatActiveToolBaseline(sessionMode).filter(name => !exposure || !WORKFLOW_OPERATOR_TOOL_IDS.has(name.toLowerCase()) || exposure.has(name.toLowerCase()));
|
|
140
142
|
if (!session?.getAllTools)
|
|
141
143
|
return baseline;
|
|
142
144
|
const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
|
|
@@ -148,10 +150,12 @@ export function computeChatActiveToolNames(session, sessionMode) {
|
|
|
148
150
|
const lower = name.toLowerCase();
|
|
149
151
|
if (baselineLower.has(lower))
|
|
150
152
|
continue;
|
|
151
|
-
if (CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
|
|
153
|
+
if (CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower) || isOperatorChatToolDenied(name))
|
|
152
154
|
continue;
|
|
153
155
|
if (sessionMode === "pi" && WORKFLOW_OPERATOR_TOOL_IDS.has(lower))
|
|
154
156
|
continue;
|
|
157
|
+
if (exposure && WORKFLOW_OPERATOR_TOOL_IDS.has(lower) && !exposure.has(lower))
|
|
158
|
+
continue;
|
|
155
159
|
extras.push(name);
|
|
156
160
|
}
|
|
157
161
|
return extras.length > 0 ? [...baseline, ...extras] : baseline;
|