@worca/app 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -15,18 +15,27 @@ import { rm, readFile } from 'node:fs/promises';
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
RunHarness, isAbort, isPause, pauseErr, firstLine, jsonClone,
|
|
18
|
-
clipMiddle, sumStepActive, normalizeClarifyAnswer,
|
|
18
|
+
clipMiddle, sumStepActive, normalizeClarifyAnswer, findDisabledPluginFor,
|
|
19
19
|
} from './run-harness.mjs';
|
|
20
20
|
import { resolveGraph, loadAgentFile, GRAPH_DEFAULT_WORKFLOW, writeGraphWorkflow, readWorkflow } from './workflows.mjs';
|
|
21
|
+
import { loadScriptRegistry } from './script-registry.mjs';
|
|
21
22
|
import { AUTO_WORKFLOW_ID, AUTO_WORKFLOW_NAME } from './graph/builtin-workflows.mjs';
|
|
22
23
|
import { classifyLoops } from '../shared/graph/loops.mjs';
|
|
23
24
|
import { buildGraphManifest, manifestTemplate, manifestPortsFn } from '../shared/graph/manifest.mjs';
|
|
24
|
-
import { DEFAULT_MAX_CYCLES } from '../shared/graph/constants.mjs';
|
|
25
|
+
import { DEFAULT_MAX_CYCLES, KEYED_KINDS } from '../shared/graph/constants.mjs';
|
|
26
|
+
import { scriptNodeCtx, pythonMissingSentence } from '../shared/graph/script-meta.mjs';
|
|
27
|
+
import { probePython } from './graph/python-probe.mjs';
|
|
28
|
+
import { mockEnabled } from './claude-runner.mjs';
|
|
25
29
|
import { registryPortsFn } from './graph/registry-ports.mjs';
|
|
26
30
|
import { createScheduler, sliceExecutionId, QUIESCENCE_WARNING } from './graph/scheduler.mjs';
|
|
27
31
|
import { runExecution, allocateOutputs, allocateVerdict, readDecomposition } from './graph/executor.mjs';
|
|
32
|
+
import { serialQueue, measureCodeCursor, collectStepEvidence } from './graph/human-evidence.mjs';
|
|
33
|
+
import { estimateStepHours, resolveConstants, sumStepHours } from '../shared/human-estimate.mjs';
|
|
34
|
+
import { diffNumstat } from './git-info.mjs';
|
|
35
|
+
import { humanEstimateOverrides, memoryDefragModel } from './settings.mjs';
|
|
28
36
|
import { renderPromptArtifact } from './phases.mjs';
|
|
29
37
|
import { listModels, modelHasBaseUrlRouting, resolveRunConfig } from './config.mjs';
|
|
38
|
+
import { resolveDefragModel, agentPairText } from './memory-defrag-model.mjs';
|
|
30
39
|
import { assembleShape, ShapeError } from '../shared/graph/assemble.mjs';
|
|
31
40
|
import { fingerprintProject } from './auto/fingerprint.mjs';
|
|
32
41
|
import { classifyTask, ClassifierError } from './auto/classify.mjs';
|
|
@@ -37,7 +46,8 @@ import {
|
|
|
37
46
|
appendAudit, writeReview, reviewKindOf, writeDecomposition, updateTaskStatus,
|
|
38
47
|
updatePhaseStatus, writeStepQuestions, readStepQuestions,
|
|
39
48
|
} from './artifacts.mjs';
|
|
40
|
-
import {
|
|
49
|
+
import { readAskFile } from './protocol.mjs';
|
|
50
|
+
import { prepareFormAsk, formAnswerValidator, downgradeQuestion } from './ask-forms.mjs';
|
|
41
51
|
import { classifyError } from './recoverable-error.mjs';
|
|
42
52
|
import { resolveFailure, markTerminal, isTerminal } from './failure-policy.mjs';
|
|
43
53
|
|
|
@@ -67,12 +77,16 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
67
77
|
if (!this.opts.workflowId) this.workflowId = GRAPH_DEFAULT_WORKFLOW.id;
|
|
68
78
|
// (this._runners is assigned by the _initRunners hook the base constructor calls.)
|
|
69
79
|
this.resolved = null; // resolveGraph's { template, ports, loops, nodes→nodeCtx, wires, agentsByKey, agentKeys }
|
|
80
|
+
this.scriptRegistry = null; // loadScriptRegistry() for this run (D16-filtered); tests override the built-in dir with opts.scriptsDir
|
|
70
81
|
this._scheduler = null;
|
|
71
82
|
this._graphSnapshot = null; // last CLEAN scheduler snapshot
|
|
72
83
|
this._resumeSnapshot = null; // the snapshot a resume restores from
|
|
73
84
|
this._resumeSessions = null; // Map executionId -> sessionId (one-shot)
|
|
74
85
|
this._graphError = null; // first genuine execution error (identity preserved)
|
|
75
86
|
this._planVersion = 0; // {vsuffix} ticks, carried across a resume
|
|
87
|
+
this._humanCursor = null; // cumulative worktree numstat at the last agent/script terminal (money-saved §3.1)
|
|
88
|
+
this._humanCursorReady = false; // baseline measured at the first agent/script start, or restored from the resume point
|
|
89
|
+
this._humanRun = serialQueue(); // measure-then-credit is atomic per execution: slices that end together split, never double
|
|
76
90
|
this._taskArtifact = null; // the pre-rendered task document
|
|
77
91
|
this.extrasFiles = [];
|
|
78
92
|
// Auto workflow (spec §5): the decision loop's state. `feedback`/`round`/`prior`
|
|
@@ -111,16 +125,32 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
111
125
|
* @returns {Promise<{manifest:object, agentKeys:Set<string>, workflow:{id:string,name:string}}>}
|
|
112
126
|
*/
|
|
113
127
|
async _resolveTopology(registry) {
|
|
128
|
+
this.scriptRegistry = loadScriptRegistry({ scriptsDir: this.opts.scriptsDir, agentKeys: Object.keys(registry || {}) });
|
|
114
129
|
if (this.workflowId === AUTO_WORKFLOW_ID) return this._autoBootstrapTopology();
|
|
130
|
+
// Settings › Memory: a defragment run (memoryScope ⇔ wf_memory_defrag, checked in the
|
|
131
|
+
// constructor) resolves its model/effort pair HERE — the one place every entry point reaches
|
|
132
|
+
// (the Memory view's button, New pipeline, an Ask card, a schedule and the CLI all construct
|
|
133
|
+
// this class). resume() never calls this hook: the manifest froze the pair on the node.
|
|
134
|
+
const defrag = this.memoryScope ? await this._defragAgentPair() : null;
|
|
115
135
|
const resolved = await resolveGraph(this.projectDir, this.workflowId, registry, this.agentsDir, {
|
|
116
|
-
isWorkspace: this.isWorkspace,
|
|
136
|
+
isWorkspace: this.isWorkspace, scripts: this.scriptRegistry, ...(defrag && defrag.pair ? { agentPair: defrag.pair } : {}),
|
|
117
137
|
});
|
|
118
138
|
this._adoptResolvedGraph(resolved);
|
|
139
|
+
// A setting that failed the catalog check degrades — and says what the run uses INSTEAD, read
|
|
140
|
+
// off the resolved graph (a project's own pick, a team default or the template's model): a run
|
|
141
|
+
// log line, and an audit line queued until the pipeline dir exists (RunHarness._pendingAudits).
|
|
142
|
+
if (defrag && defrag.warning) {
|
|
143
|
+
const text = `${defrag.warning} — the run uses ${agentPairText(this.resolved.nodeCtx)}`;
|
|
144
|
+
this._log('orchestrator', 'warn', text);
|
|
145
|
+
this._pendingAudits.push(`${text}.`);
|
|
146
|
+
}
|
|
147
|
+
this._preflightScriptKeys(this.resolved.scriptKeys);
|
|
148
|
+
await this._preflightScriptRuntimes();
|
|
119
149
|
// The manifest is built from the RESOLVED template, the resolver's registry
|
|
120
150
|
// slice and its EFFECTIVE per-node/per-wire values (P2 contract): the run
|
|
121
151
|
// monitor shows exactly what the engine will run.
|
|
122
152
|
const manifest = buildGraphManifest(this.resolved.template, this.resolved.agentsByKey, {
|
|
123
|
-
overlays: { nodes: this.resolved.nodeCtx, wires: this.resolved.wires },
|
|
153
|
+
overlays: { nodes: this.resolved.nodeCtx, wires: this.resolved.wires }, scripts: this.resolved.scriptsByKey,
|
|
124
154
|
});
|
|
125
155
|
return {
|
|
126
156
|
manifest,
|
|
@@ -129,6 +159,27 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
129
159
|
};
|
|
130
160
|
}
|
|
131
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Settings › Memory: the pair every agent node of this Memory defragment run uses
|
|
164
|
+
* (memory-defrag-model.mjs) — the one named at start (`claude.model` / `claude.effort`: the
|
|
165
|
+
* CLI's --model, a CLI-made schedule) wins, else the stored setting, validated against THIS
|
|
166
|
+
* project's catalog. A setting that fails the check comes back as `warning` (the caller logs it
|
|
167
|
+
* once the graph says what the run uses instead) — never a refused run. `pair: null` ⇒ the node
|
|
168
|
+
* layers resolve exactly as before.
|
|
169
|
+
* @returns {Promise<{pair: ({model:string, effort:(string|null)}|null), warning: (string|null)}>}
|
|
170
|
+
*/
|
|
171
|
+
async _defragAgentPair() {
|
|
172
|
+
const explicit = { model: this.claude.model, effort: this.opts.claude?.effort };
|
|
173
|
+
const stored = memoryDefragModel();
|
|
174
|
+
// The catalog read only when the setting is the one that decides.
|
|
175
|
+
const models = !(typeof explicit.model === 'string' && explicit.model.trim()) && stored.model
|
|
176
|
+
? await listModels(this.projectDir) : [];
|
|
177
|
+
const r = resolveDefragModel({ explicit, stored, models });
|
|
178
|
+
if (!r.model) return { pair: null, warning: r.warning };
|
|
179
|
+
this._log('orchestrator', 'info', `Memory defragment model: ${r.model}${r.effort ? ` · ${r.effort}` : ''} (${r.source === 'explicit' ? 'named at start' : 'Settings › Memory'})`);
|
|
180
|
+
return { pair: { model: r.model, effort: r.effort }, warning: r.warning };
|
|
181
|
+
}
|
|
182
|
+
|
|
132
183
|
/** The Auto entry before the decision: an EMPTY graph tagged `deciding`, so
|
|
133
184
|
* the run row, the Running page and a pre-decision resume point all have a
|
|
134
185
|
* manifest to carry (spec §5.2). */
|
|
@@ -387,7 +438,7 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
387
438
|
for (const [nodeId, sel] of Object.entries(tunables || {})) overlayNodes[nodeId] = { ...sel };
|
|
388
439
|
for (const [nodeId, sel] of Object.entries(answer.nodes || {})) overlayNodes[nodeId] = { ...(overlayNodes[nodeId] || {}), ...sel };
|
|
389
440
|
const resolved = await resolveGraph(this.projectDir, workflowId, registry, this.agentsDir, {
|
|
390
|
-
isWorkspace: false, overlay: { nodes: overlayNodes }, ignoreProjectOverrides: true,
|
|
441
|
+
isWorkspace: false, overlay: { nodes: overlayNodes }, ignoreProjectOverrides: true, scripts: this.scriptRegistry,
|
|
391
442
|
});
|
|
392
443
|
if (!this.humanInLoop) {
|
|
393
444
|
// spec D3: no agent may stop the run to ask (generic — every agent node).
|
|
@@ -396,10 +447,12 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
396
447
|
this.workflowId = workflowId;
|
|
397
448
|
this._adoptResolvedGraph(resolved);
|
|
398
449
|
const manifest = buildGraphManifest(this.resolved.template, this.resolved.agentsByKey, {
|
|
399
|
-
overlays: { nodes: this.resolved.nodeCtx, wires: this.resolved.wires },
|
|
450
|
+
overlays: { nodes: this.resolved.nodeCtx, wires: this.resolved.wires }, scripts: this.resolved.scriptsByKey,
|
|
400
451
|
});
|
|
401
452
|
manifest.auto = { status: 'decided', via, rounds: round, humanInLoop: this.humanInLoop, workflowId };
|
|
402
453
|
this._preflightAgentKeys(this.resolved.agentKeys);
|
|
454
|
+
this._preflightScriptKeys(this.resolved.scriptKeys);
|
|
455
|
+
await this._preflightScriptRuntimes();
|
|
403
456
|
this.state.stepper = manifest;
|
|
404
457
|
// PR #434 review, finding 3: the pending proposal is kept until HERE. A throw before the
|
|
405
458
|
// workflowId swap above (mintAutoWorkflowId, writeGraphWorkflow, resolveGraph) unwinds
|
|
@@ -466,7 +519,7 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
466
519
|
|
|
467
520
|
/**
|
|
468
521
|
* Adopt a resolveGraph result (P2 contract: { template, ports, loops, nodes,
|
|
469
|
-
* wires, agentsByKey, agentKeys }). The resolver has ALREADY applied the
|
|
522
|
+
* wires, agentsByKey, agentKeys, scriptsByKey, scriptKeys }). The resolver has ALREADY applied the
|
|
470
523
|
* workspace substitution AND the workspaceFanOut forcing (spec §5.10 — a META
|
|
471
524
|
* flag, never a key set) and classified the loops ONCE. This class names the
|
|
472
525
|
* per-node table `nodeCtx`; nothing is re-derived and no template node is
|
|
@@ -516,6 +569,50 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
516
569
|
return manifest ? new Set(resolvedFromManifest(manifest, this.registry).agentKeys) : new Set();
|
|
517
570
|
}
|
|
518
571
|
|
|
572
|
+
/** §8.3: every script key must resolve in this run's script registry BEFORE any
|
|
573
|
+
* node executes — the mirror of the base's _preflightAgentKeys, with the same
|
|
574
|
+
* disabled-plugin hint. resolveGraph already refuses an unknown key on a fresh
|
|
575
|
+
* run; this is what catches a plugin withdrawn while the run sat paused. */
|
|
576
|
+
_preflightScriptKeys(scriptKeys) {
|
|
577
|
+
const reg = this.scriptRegistry || {};
|
|
578
|
+
const missing = [];
|
|
579
|
+
for (const key of new Set(scriptKeys || [])) {
|
|
580
|
+
if (!key || Object.hasOwn(reg, key)) continue;
|
|
581
|
+
const plugin = findDisabledPluginFor(key, 'scripts');
|
|
582
|
+
missing.push(plugin
|
|
583
|
+
? `script "${key}" comes from disabled plugin "${plugin}" — enable it`
|
|
584
|
+
: `script "${key}" is not installed (removed plugin?)`);
|
|
585
|
+
}
|
|
586
|
+
if (missing.length) {
|
|
587
|
+
throw new Error(`Preflight failed: ${missing.length} workflow script key(s) do not resolve:\n` + missing.map((m) => ` - ${m}`).join('\n'));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Workbench spec §7: a `python` card needs an interpreter on THIS host. That is
|
|
593
|
+
* a run-time fact — the probe is async and the registry loader is not — so it is
|
|
594
|
+
* checked HERE, beside the key preflight, before the pipeline dir exists and
|
|
595
|
+
* long before the first execution, and is never baked into a registry snapshot.
|
|
596
|
+
* The message is the §7 sentence itself (one line per distinct key, first-seen
|
|
597
|
+
* order): for the usual single python card it is EXACTLY that sentence, which
|
|
598
|
+
* the bench, the composer's V4 and the CLI all repeat word for word.
|
|
599
|
+
*/
|
|
600
|
+
async _preflightScriptRuntimes() {
|
|
601
|
+
// D13: in a mock run a card with a DECLARED mock spawns nothing (runScriptExecution returns before it
|
|
602
|
+
// probes), so it needs no interpreter — the same condition, read the same way.
|
|
603
|
+
const mocked = mockEnabled({ mock: this.claude?.mock });
|
|
604
|
+
const keys = [];
|
|
605
|
+
for (const nc of Object.values(this.resolved?.nodeCtx || {})) {
|
|
606
|
+
if (nc?.kind !== 'script' || nc.runtime !== 'python' || !nc.key || keys.includes(nc.key)) continue;
|
|
607
|
+
if (mocked && nc.mock && typeof nc.mock === 'object') continue;
|
|
608
|
+
keys.push(nc.key);
|
|
609
|
+
}
|
|
610
|
+
if (!keys.length) return;
|
|
611
|
+
const probe = await probePython();
|
|
612
|
+
if (probe.ok) return;
|
|
613
|
+
throw new Error(keys.map((key) => pythonMissingSentence(key)).join('\n'));
|
|
614
|
+
}
|
|
615
|
+
|
|
519
616
|
// ── hook 2: run the graph ──────────────────────────────────────────────────
|
|
520
617
|
/**
|
|
521
618
|
* The scheduler owns readiness, loop budgets, gates and End; this method owns
|
|
@@ -542,7 +639,9 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
542
639
|
// every entry agent binds that same file. Byte-identical to v1's seeded task
|
|
543
640
|
// file (the same renderer), so the Task card's document matches what v1
|
|
544
641
|
// handed its entry node.
|
|
545
|
-
|
|
642
|
+
// A Memory defragment run appends the scope's health (run-harness.mjs _defragBrief — '' on
|
|
643
|
+
// every other run, so their document stays byte-identical).
|
|
644
|
+
this._taskArtifact = { text: renderPromptArtifact(this.pipeline.promptText, this.extrasFiles) + await this._defragBrief() };
|
|
546
645
|
|
|
547
646
|
const sched = createScheduler({
|
|
548
647
|
template: this._schedulerTemplate(),
|
|
@@ -648,6 +747,9 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
648
747
|
completed: s.status === 'done',
|
|
649
748
|
})),
|
|
650
749
|
planVersion: this._planVersion,
|
|
750
|
+
// money-saved §3.1: the cursor at the last completed terminal (paused executions never
|
|
751
|
+
// advance it), so a resume credits the paused execution's pre-pause work at its terminal.
|
|
752
|
+
humanCursor: this._humanCursorReady ? (this._humanCursor ?? { files: 0, insertions: 0, deletions: 0 }) : null,
|
|
651
753
|
stepModels: this.stepModels,
|
|
652
754
|
workflowId: this.workflowId,
|
|
653
755
|
// Auto workflow: the decision state while UNDECIDED (spec §5.6); null once
|
|
@@ -755,7 +857,11 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
755
857
|
});
|
|
756
858
|
}
|
|
757
859
|
}
|
|
758
|
-
this._emit('exec', {
|
|
860
|
+
this._emit('exec', {
|
|
861
|
+
...payload, costUsd: step ? (step.costUsd || 0) : 0,
|
|
862
|
+
...(step && step.runtime != null ? { runtime: step.runtime } : {}),
|
|
863
|
+
...(step && step.exitCode != null ? { exitCode: step.exitCode } : {}),
|
|
864
|
+
});
|
|
759
865
|
}
|
|
760
866
|
|
|
761
867
|
/**
|
|
@@ -835,6 +941,7 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
835
941
|
} catch (err) {
|
|
836
942
|
return this._settleUnstarted(nc, node, args, err); // allocation failed: no row to mark
|
|
837
943
|
}
|
|
944
|
+
await this._humanCursorInit(ctx);
|
|
838
945
|
this._execStep(ctx, 'start');
|
|
839
946
|
let endMark = 'done';
|
|
840
947
|
try {
|
|
@@ -847,8 +954,16 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
847
954
|
// here, not on the next spawn.
|
|
848
955
|
this._checkAbort();
|
|
849
956
|
this._checkPause();
|
|
850
|
-
if (node.kind
|
|
851
|
-
this._checkCostLimits(); // budget gate at EVERY
|
|
957
|
+
if (!KEYED_KINDS.includes(node.kind)) return await this._runFlow(ctx);
|
|
958
|
+
this._checkCostLimits(); // budget gate at EVERY spawn (throws pauseErr)
|
|
959
|
+
if (node.kind === 'script') {
|
|
960
|
+
// A child process through the NODE site: every runner error carries
|
|
961
|
+
// errorClass:null (D9), so _recover lands on the '*' row — pause as
|
|
962
|
+
// REASON.ERROR, resumable, never a "network" retry. No questions, no session.
|
|
963
|
+
const result = await this._runNodeAttempts(nc, ctx);
|
|
964
|
+
await this._afterExecution(nc, ctx, result);
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
852
967
|
this._primeQuestions(nc, ctx);
|
|
853
968
|
let result = await this._runNodeAttempts(nc, ctx);
|
|
854
969
|
result = await this._questionsLoop(nc, ctx, result);
|
|
@@ -909,6 +1024,7 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
909
1024
|
}
|
|
910
1025
|
throw err;
|
|
911
1026
|
} finally {
|
|
1027
|
+
if (endMark !== 'paused') await this._humanEstimate(ctx);
|
|
912
1028
|
this._execStep(ctx, endMark);
|
|
913
1029
|
// A PAUSED slice stays 'running': the resume re-runs the whole composite,
|
|
914
1030
|
// and a task that never finished must not read as done.
|
|
@@ -992,7 +1108,8 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
992
1108
|
stepIndex: null,
|
|
993
1109
|
cycle: ordinal,
|
|
994
1110
|
uiPhase: this._uiPhaseOf(node.id),
|
|
995
|
-
model:
|
|
1111
|
+
// A script has no model: no per-model cost override, no cost-reliability observation.
|
|
1112
|
+
model: nc.kind === 'script' ? null : (nc.model || this.claude.model),
|
|
996
1113
|
};
|
|
997
1114
|
return {
|
|
998
1115
|
// Consumed as `cwd` by phases.mjs (runOpts). runCwd is the run root on a
|
|
@@ -1007,8 +1124,9 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1007
1124
|
pipelineId: this.pipeline.id,
|
|
1008
1125
|
taskPrompt: this.pipeline.promptText,
|
|
1009
1126
|
toolInstruction: this.toolInstruction,
|
|
1010
|
-
memoryBlock: this.memoryBlock || '', // §4.3: the pointer block, rendered once per mount
|
|
1011
|
-
memoryMount: this.memory?.mount || null, //
|
|
1127
|
+
memoryBlock: this.memoryBlock || '', // §4.3: the pointer block, rendered once per mount (names the WRITABLE dirs)
|
|
1128
|
+
memoryMount: this.memory?.mount || null, // the WRITABLE copy: <pipeline.dir>/memory (agents, the defrag mock and tests write here; runOpts passes it as --add-dir)
|
|
1129
|
+
memoryRules: this.memory?.rules || null, // the read-only rules copy inside the cwd (tests read it; agents never write it)
|
|
1012
1130
|
agentPrompts: this.agentPrompts,
|
|
1013
1131
|
checkpointRef: this.checkpointRef,
|
|
1014
1132
|
workspace: this.isWorkspace ? this._workspaceChannel() : undefined,
|
|
@@ -1030,7 +1148,7 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1030
1148
|
// model the spawn will actually use, global default included. Live
|
|
1031
1149
|
// catalog on purpose — a resume re-resolves the env the same way. One
|
|
1032
1150
|
// settings + plugins-lock read per dispatch; never call this per entry.
|
|
1033
|
-
endpointRouted: modelHasBaseUrlRouting(nc.model || this.claude.model),
|
|
1151
|
+
endpointRouted: nc.kind === 'agent' ? modelHasBaseUrlRouting(nc.model || this.claude.model) : false,
|
|
1034
1152
|
agentPrompt: nc.agentPrompt,
|
|
1035
1153
|
tools: nc.tools, // frontmatter grants MUST be stamped
|
|
1036
1154
|
promptHints: nc.promptHints || '',
|
|
@@ -1053,6 +1171,10 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1053
1171
|
outputs,
|
|
1054
1172
|
verdict,
|
|
1055
1173
|
runCtx,
|
|
1174
|
+
// The script contract (spec §6.1): what script-runner.mjs spawns. Absent on every other kind.
|
|
1175
|
+
script: nc.kind === 'script'
|
|
1176
|
+
? { meta: nc.meta, runtime: nc.runtime, file: nc.file, command: nc.command, params: nc.params, paramsPort: nc.paramsPort === true, timeoutMs: nc.timeoutMs, mock: nc.mock }
|
|
1177
|
+
: undefined,
|
|
1056
1178
|
runners: this._runners, // P3's injection seam (runExecution reads ctx.runners)
|
|
1057
1179
|
resumeSessionId: this._takeResumeSession(executionId),
|
|
1058
1180
|
ask: (q) => this._enqueueAsk(() => this._ask(q)),
|
|
@@ -1108,7 +1230,8 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1108
1230
|
kind: ctx.slice ? 'task' : 'cycle',
|
|
1109
1231
|
ordinal: ctx.ordinal,
|
|
1110
1232
|
cycle: ctx.ordinal, // legacy alias the whole UI reads
|
|
1111
|
-
agentKey: ctx.node?.key ?? null,
|
|
1233
|
+
agentKey: ctx.node?.kind === 'agent' ? (ctx.node.key ?? null) : null, // agents only (D17: it must not lie)
|
|
1234
|
+
nodeKey: ctx.node?.key ?? null, // every keyed kind
|
|
1112
1235
|
phase: ctx.node?.key ?? ctx.uiPhase, // legacy column
|
|
1113
1236
|
stepIndex: null, // a graph has executions, not step indexes
|
|
1114
1237
|
status,
|
|
@@ -1135,6 +1258,11 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1135
1258
|
if (status === 'start') step.endedAt = null;
|
|
1136
1259
|
}
|
|
1137
1260
|
if (terminal) step.endedAt = now;
|
|
1261
|
+
if (terminal && ctx.human) {
|
|
1262
|
+
step.humanHours = ctx.human.hours;
|
|
1263
|
+
step.humanSignals = ctx.human.signals;
|
|
1264
|
+
}
|
|
1265
|
+
if (terminal) this.state.humanHours = sumStepHours(this.state.steps);
|
|
1138
1266
|
if (status === 'start') this._clockResume(key);
|
|
1139
1267
|
else this._clockPause(key);
|
|
1140
1268
|
this.state.totalActiveMs = sumStepActive(this.state.steps);
|
|
@@ -1161,6 +1289,49 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1161
1289
|
this._persist().catch(() => {});
|
|
1162
1290
|
}
|
|
1163
1291
|
|
|
1292
|
+
/** Intent-to-add every member worktree, then measure. A NEW file is invisible to
|
|
1293
|
+
* `git diff <checkpoint>` until `git add -N` has seen it (the harness stages the same
|
|
1294
|
+
* way before a review); ignoreAbort so the stopped path still measures. Never throws. */
|
|
1295
|
+
async _humanMeasure() {
|
|
1296
|
+
try {
|
|
1297
|
+
return await measureCodeCursor({
|
|
1298
|
+
workDirs: this.workDirs, checkpointRefs: this.checkpointRefs,
|
|
1299
|
+
excludeFor: (k) => this._excludePathspecs(k), numstat: diffNumstat,
|
|
1300
|
+
stage: () => this._stageWorkingTree({ ignoreAbort: true }),
|
|
1301
|
+
});
|
|
1302
|
+
} catch { return null; }
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** The baseline at the FIRST agent/script start of a run: under legacy run-root mode the
|
|
1306
|
+
* checkout may already be dirty, and that dirt is not the run's work. A resume restores
|
|
1307
|
+
* the cursor from the resume point instead, so the paused execution's pre-pause work
|
|
1308
|
+
* is credited at its real terminal. Queued, so it lands before any estimate. */
|
|
1309
|
+
_humanCursorInit(ctx) {
|
|
1310
|
+
const kind = ctx?.node?.kind;
|
|
1311
|
+
if (this._humanCursorReady || (kind !== 'agent' && kind !== 'script')) return Promise.resolve();
|
|
1312
|
+
this._humanCursorReady = true;
|
|
1313
|
+
return this._humanRun(async () => { this._humanCursor = await this._humanMeasure(); }).catch(() => {});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/** Evidence → hours for ONE terminal execution, serialized with every other measurement of
|
|
1317
|
+
* this run. Never rejects. Flow cards and scripts leave ctx.human null (their row stays
|
|
1318
|
+
* NULL: "not estimated" is not 0); a script still advances the baseline so its own file
|
|
1319
|
+
* changes are never credited to the next agent. */
|
|
1320
|
+
_humanEstimate(ctx) {
|
|
1321
|
+
ctx.human = null;
|
|
1322
|
+
const kind = ctx?.node?.kind;
|
|
1323
|
+
if (kind !== 'agent' && kind !== 'script') return Promise.resolve();
|
|
1324
|
+
return this._humanRun(async () => {
|
|
1325
|
+
const cursorPrev = this._humanCursor;
|
|
1326
|
+
const measured = await this._humanMeasure();
|
|
1327
|
+
if (measured) this._humanCursor = measured;
|
|
1328
|
+
if (kind !== 'agent') return;
|
|
1329
|
+
const evidence = await collectStepEvidence({ ctx, cursorPrev, cursorNow: measured || cursorPrev });
|
|
1330
|
+
const est = estimateStepHours(evidence, resolveConstants(humanEstimateOverrides()));
|
|
1331
|
+
ctx.human = { hours: est.hours, signals: { ...est.signals, method: est.method } };
|
|
1332
|
+
}).catch(() => { ctx.human = null; });
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1164
1335
|
/** The retry loop around ONE execution — the NODE site of failure-policy.mjs.
|
|
1165
1336
|
* _recover() resolves the verdict (running the backoff or the recovery prompt
|
|
1166
1337
|
* on the way); this loop enacts it. A pause throws pauseErr() with
|
|
@@ -1241,6 +1412,18 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1241
1412
|
nodeId: ctx.nodeId, executionId: ctx.executionId, port: port.id, cycle: ctx.ordinal,
|
|
1242
1413
|
});
|
|
1243
1414
|
}
|
|
1415
|
+
if (nc.kind === 'script') {
|
|
1416
|
+
// The envelope audit copy is an artifact under scripts/ (§6.5); the row gets the runtime facts (D17).
|
|
1417
|
+
if (result?.envelopePath) {
|
|
1418
|
+
this._artifact('envelope', result.envelopePath, { nodeId: ctx.nodeId, executionId: ctx.executionId, port: null, cycle: ctx.ordinal });
|
|
1419
|
+
}
|
|
1420
|
+
const step = this.state.steps.find((s) => s.key === ctx.executionId);
|
|
1421
|
+
if (step) {
|
|
1422
|
+
step.runtime = result?.runtime ?? nc.runtime ?? null;
|
|
1423
|
+
step.exitCode = result?.exitCode ?? null;
|
|
1424
|
+
}
|
|
1425
|
+
return; // no memory sync, no worktree staging
|
|
1426
|
+
}
|
|
1244
1427
|
// Agent memory (§5): sync the mount back after EVERY execution, slices included.
|
|
1245
1428
|
await this._syncMemory(nc, ctx);
|
|
1246
1429
|
if (nc.meta?.sideEffect === 'code' && !ctx.slice) await this._stageWorkingTree();
|
|
@@ -1272,6 +1455,14 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1272
1455
|
ctx.questionsAnswered = readStepQuestions(this.pipeline.id)
|
|
1273
1456
|
.filter((r) => r.nodeId === ctx.nodeId)
|
|
1274
1457
|
.flatMap((r) => r.answers);
|
|
1458
|
+
// Spec §4: the forms this agent may ask with (absent for every agent that
|
|
1459
|
+
// declares none, which keeps questionsPromptBlock byte-identical), and the
|
|
1460
|
+
// form answers already given for this NODE — same node-scoped filter as the
|
|
1461
|
+
// legacy answers above, for the same reason (a fix cycle is a new execution).
|
|
1462
|
+
ctx.askForms = nc.meta?.ask?.forms || null;
|
|
1463
|
+
ctx.formAnswers = readStepQuestions(this.pipeline.id)
|
|
1464
|
+
.filter((r) => r.nodeId === ctx.nodeId && r.formAnswer)
|
|
1465
|
+
.map((r) => r.formAnswer);
|
|
1275
1466
|
ctx.questionsFile = this._questionsPath(ctx.nodeId, ctx.ordinal, 1);
|
|
1276
1467
|
}
|
|
1277
1468
|
|
|
@@ -1289,6 +1480,66 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1289
1480
|
return join(this.pipeline.dir, `questions-x-${nodeIdSafe}-c${ordinal}-r${round}.json`);
|
|
1290
1481
|
}
|
|
1291
1482
|
|
|
1483
|
+
/**
|
|
1484
|
+
* Gate 2 for a `{form,data}` ask (spec §5). Prepares the ask; on a refusal the
|
|
1485
|
+
* agent is resumed ONCE with the exact error list plus the data schema and the
|
|
1486
|
+
* SAME round file, and its retry is re-read. A second refusal downgrades to one
|
|
1487
|
+
* generic free-text question built from the form's title, and the run log says
|
|
1488
|
+
* why. Never throws, and never consumes a question round — MAX_QUESTION_ROUNDS
|
|
1489
|
+
* still bounds what the USER sees.
|
|
1490
|
+
* @returns {Promise<{ask:object|null, autoValues:object|null, questions:Array, result:object|undefined}>}
|
|
1491
|
+
*/
|
|
1492
|
+
async _prepareFormAsk(nc, ctx, first, qPath, round, agentLabel) {
|
|
1493
|
+
const attr = { nodeId: ctx.nodeId, executionId: ctx.executionId, cycle: ctx.ordinal };
|
|
1494
|
+
let payload = first;
|
|
1495
|
+
let result;
|
|
1496
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1497
|
+
const prepared = await prepareFormAsk({
|
|
1498
|
+
agentMeta: nc.meta,
|
|
1499
|
+
payload: { form: payload.form, data: payload.data },
|
|
1500
|
+
cwd: ctx.projectDir,
|
|
1501
|
+
pipelineDir: this.pipeline.dir,
|
|
1502
|
+
askId: `questions-${ctx.executionId}-r${round}`,
|
|
1503
|
+
});
|
|
1504
|
+
if (prepared.ok) return { ask: prepared.ask, autoValues: prepared.autoValues, questions: [], result };
|
|
1505
|
+
|
|
1506
|
+
const why = prepared.errors.map((e) => `${e.path ? `${e.path}: ` : ''}${e.message}`).join('; ');
|
|
1507
|
+
this._log(agentLabel, 'warn', `form "${payload.form}" was refused: ${why}`, attr);
|
|
1508
|
+
await appendAudit(this.pipeline.dir,
|
|
1509
|
+
`${agentLabel}: form "${payload.form}" was refused — ${why}`).catch(() => {});
|
|
1510
|
+
if (attempt === 2) break;
|
|
1511
|
+
|
|
1512
|
+
// ONE repair round: the errors + the schema, and the SAME file to rewrite.
|
|
1513
|
+
ctx.formRepair = {
|
|
1514
|
+
form: payload.form,
|
|
1515
|
+
errors: prepared.errors,
|
|
1516
|
+
schema: nc.meta?.ask?.forms?.[payload.form]?.data || null,
|
|
1517
|
+
file: qPath,
|
|
1518
|
+
};
|
|
1519
|
+
await rm(qPath, { force: true }).catch(() => {});
|
|
1520
|
+
const step = this.state.steps.find((s) => s.key === ctx.executionId);
|
|
1521
|
+
if (step?.sessionId) ctx.resumeSessionId = step.sessionId;
|
|
1522
|
+
try {
|
|
1523
|
+
result = await this._runNodeAttempts(nc, ctx);
|
|
1524
|
+
} finally {
|
|
1525
|
+
ctx.formRepair = null;
|
|
1526
|
+
}
|
|
1527
|
+
this._checkAbort();
|
|
1528
|
+
const retry = await readAskFile(qPath);
|
|
1529
|
+
// The agent may give up on the form and write plain questions instead (or
|
|
1530
|
+
// write nothing): take whatever it DID write, exactly as a legacy round would.
|
|
1531
|
+
if (retry.kind !== 'form') {
|
|
1532
|
+
return { ask: null, autoValues: null, questions: retry.kind === 'questions' ? retry.questions : [], result };
|
|
1533
|
+
}
|
|
1534
|
+
payload = retry;
|
|
1535
|
+
}
|
|
1536
|
+
const title = nc.meta?.ask?.forms?.[payload.form]?.title || '';
|
|
1537
|
+
this._log(agentLabel, 'warn', `form "${payload.form}" downgraded to a free-text question`, attr);
|
|
1538
|
+
await appendAudit(this.pipeline.dir,
|
|
1539
|
+
`${agentLabel}: form "${payload.form}" downgraded to a free-text question after two refusals.`).catch(() => {});
|
|
1540
|
+
return { ask: null, autoValues: null, questions: [downgradeQuestion({ form: payload.form, title })], result };
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1292
1543
|
/**
|
|
1293
1544
|
* Ask-then-resume rounds. After a successful execution: if the agent wrote this
|
|
1294
1545
|
* round's questions file, persist the questions, gate the user (serialized —
|
|
@@ -1307,14 +1558,74 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1307
1558
|
for (let round = 1; round <= MAX_QUESTION_ROUNDS; round++) {
|
|
1308
1559
|
const qPath = ctx.questionsFile;
|
|
1309
1560
|
if (!qPath) break;
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1561
|
+
// `read`, not `payload`: the legacy body below still declares `const payload` for the answer.
|
|
1562
|
+
const read = await readAskFile(qPath);
|
|
1563
|
+
if (read.kind === 'none') {
|
|
1564
|
+
if (read.malformed) {
|
|
1313
1565
|
await appendAudit(this.pipeline.dir, `${agentLabel}: questions file was malformed — proceeding without asking (round ${round}).`).catch(() => {});
|
|
1314
1566
|
}
|
|
1315
1567
|
break;
|
|
1316
1568
|
}
|
|
1317
1569
|
this._checkAbort();
|
|
1570
|
+
let questions = read.kind === 'questions' ? read.questions : [];
|
|
1571
|
+
let formAsk = null;
|
|
1572
|
+
let autoValues = null;
|
|
1573
|
+
if (read.kind === 'form') {
|
|
1574
|
+
// Gate 2 (spec §5). It may spawn ONE repair round of its own, whose
|
|
1575
|
+
// result becomes this round's result; it never consumes a round.
|
|
1576
|
+
const gate = await this._prepareFormAsk(nc, ctx, read, qPath, round, agentLabel);
|
|
1577
|
+
if (gate.result !== undefined) result = gate.result;
|
|
1578
|
+
formAsk = gate.ask;
|
|
1579
|
+
autoValues = gate.autoValues;
|
|
1580
|
+
questions = gate.questions;
|
|
1581
|
+
}
|
|
1582
|
+
if (!formAsk && !questions.length) break;
|
|
1583
|
+
if (formAsk) {
|
|
1584
|
+
// §9: the persisted ask is the FULL resolved snapshot — History renders
|
|
1585
|
+
// it after the agent's sidecar changed or its plugin was removed. The
|
|
1586
|
+
// column is schemaless JSON TEXT, so nothing migrates.
|
|
1587
|
+
await writeStepQuestions(this.pipeline.id, stepKey, round, {
|
|
1588
|
+
agentKey: nc.key, nodeId: ctx.nodeId, questions: formAsk,
|
|
1589
|
+
});
|
|
1590
|
+
this._artifact('questions', qPath, { nodeId: ctx.nodeId, executionId: ctx.executionId, port: null, cycle: ctx.ordinal });
|
|
1591
|
+
await appendAudit(this.pipeline.dir, `${agentLabel} asked with form "${formAsk.form}" (round ${round}).`).catch(() => {});
|
|
1592
|
+
const answered = await this._enqueueAsk(() => this._ask({
|
|
1593
|
+
id: `questions-${stepKey}-r${round}`,
|
|
1594
|
+
kind: 'form',
|
|
1595
|
+
agent: agentLabel,
|
|
1596
|
+
nodeId: ctx.nodeId,
|
|
1597
|
+
executionId: ctx.executionId,
|
|
1598
|
+
askId: formAsk.askId, // ruling X1: the ROUTE token, not `id`
|
|
1599
|
+
form: formAsk.form,
|
|
1600
|
+
version: formAsk.version,
|
|
1601
|
+
title: formAsk.title,
|
|
1602
|
+
surface: formAsk.surface,
|
|
1603
|
+
data: formAsk.data,
|
|
1604
|
+
layout: formAsk.layout,
|
|
1605
|
+
answerSchema: formAsk.answerSchema,
|
|
1606
|
+
fileRefs: formAsk.fileRefs,
|
|
1607
|
+
files: formAsk.files,
|
|
1608
|
+
autoValues, // D10, auto mode only
|
|
1609
|
+
validate: formAnswerValidator(formAsk), // gate 3
|
|
1610
|
+
}));
|
|
1611
|
+
this._checkAbort();
|
|
1612
|
+
const values = (answered && typeof answered === 'object' && answered.values) || {};
|
|
1613
|
+
await writeStepQuestions(this.pipeline.id, stepKey, round, {
|
|
1614
|
+
agentKey: nc.key, nodeId: ctx.nodeId,
|
|
1615
|
+
answers: { kind: 'form', form: formAsk.form, version: formAsk.version, values },
|
|
1616
|
+
});
|
|
1617
|
+
await appendAudit(this.pipeline.dir, `${agentLabel}: form "${formAsk.form}" answered (round ${round}).`).catch(() => {});
|
|
1618
|
+
await rm(qPath, { force: true }).catch(() => {});
|
|
1619
|
+
const step = this.state.steps.find((s) => s.key === stepKey);
|
|
1620
|
+
if (step?.sessionId) ctx.resumeSessionId = step.sessionId;
|
|
1621
|
+
ctx.formAnswers = [...(ctx.formAnswers || []), { form: formAsk.form, version: formAsk.version, values }];
|
|
1622
|
+
ctx.questionsFile = round < MAX_QUESTION_ROUNDS
|
|
1623
|
+
? this._questionsPath(ctx.nodeId, ctx.ordinal, round + 1)
|
|
1624
|
+
: null;
|
|
1625
|
+
this._log(agentLabel, 'debug', `resuming with form "${formAsk.form}" answers (round ${round})`, attr);
|
|
1626
|
+
result = await this._runNodeAttempts(nc, ctx);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1318
1629
|
await writeStepQuestions(this.pipeline.id, stepKey, round, {
|
|
1319
1630
|
agentKey: nc.key, nodeId: ctx.nodeId, questions: { questions },
|
|
1320
1631
|
});
|
|
@@ -1458,13 +1769,17 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1458
1769
|
this._resumeSnapshot = rp.snapshot || null;
|
|
1459
1770
|
this._graphSnapshot = rp.snapshot || null;
|
|
1460
1771
|
this._planVersion = Number.isFinite(rp.planVersion) ? rp.planVersion : 0;
|
|
1772
|
+
if (rp.humanCursor && typeof rp.humanCursor === 'object') { this._humanCursor = rp.humanCursor; this._humanCursorReady = true; }
|
|
1461
1773
|
this._clearPauseReason();
|
|
1462
1774
|
const manifest = rp.manifest || this.state.stepper;
|
|
1463
1775
|
this.state.stepper = manifest;
|
|
1464
|
-
this.
|
|
1776
|
+
this.scriptRegistry = loadScriptRegistry({ scriptsDir: this.opts.scriptsDir, agentKeys: Object.keys(this.registry || {}) });
|
|
1777
|
+
this._adoptResolvedGraph(resolvedFromManifest(manifest, this.registry, this.scriptRegistry));
|
|
1465
1778
|
// §9.4, unchanged messages: the providing plugin may have been disabled or
|
|
1466
1779
|
// uninstalled while this run sat paused. (Same place v1 re-preflights.)
|
|
1467
1780
|
this._preflightAgentKeys(this.resolved.agentKeys);
|
|
1781
|
+
this._preflightScriptKeys(this.resolved.scriptKeys);
|
|
1782
|
+
await this._preflightScriptRuntimes();
|
|
1468
1783
|
// Prompt bodies + frontmatter tools: the one thing the manifest never carries.
|
|
1469
1784
|
const cache = new Map();
|
|
1470
1785
|
for (const nc of Object.values(this.resolved.nodeCtx)) {
|
|
@@ -1497,13 +1812,15 @@ export class GraphOrchestrator extends RunHarness {
|
|
|
1497
1812
|
* mockRole, displayName).
|
|
1498
1813
|
* @param {object} manifest a manifest v2
|
|
1499
1814
|
* @param {Record<string,object>} registry loadAgentRegistry() output
|
|
1500
|
-
* @
|
|
1815
|
+
* @param {Record<string,object>} [scripts] loadScriptRegistry() output (script nodes)
|
|
1816
|
+
* @returns {{template:object, ports:Function, loops:object, nodes:Record<string,object>, wires:Record<string,{maxCycles:number}>, agentsByKey:Record<string,object>, agentKeys:Set<string>, scriptsByKey:Record<string,object>, scriptKeys:Set<string>}}
|
|
1501
1817
|
*/
|
|
1502
|
-
export function resolvedFromManifest(manifest, registry) {
|
|
1818
|
+
export function resolvedFromManifest(manifest, registry, scripts = {}) {
|
|
1503
1819
|
const reg = registry && typeof registry === 'object' ? registry : {};
|
|
1820
|
+
const scr = scripts && typeof scripts === 'object' ? scripts : {};
|
|
1504
1821
|
const template = manifestTemplate(manifest); // restores node.config + loop wire config.maxCycles verbatim
|
|
1505
1822
|
const manPorts = manifestPortsFn(manifest);
|
|
1506
|
-
const regPorts = registryPortsFn(reg);
|
|
1823
|
+
const regPorts = registryPortsFn(reg, scr);
|
|
1507
1824
|
const ports = (node) => {
|
|
1508
1825
|
const snap = manPorts(node);
|
|
1509
1826
|
const live = regPorts(node) || { inputs: [], outputs: [] };
|
|
@@ -1522,6 +1839,13 @@ export function resolvedFromManifest(manifest, registry) {
|
|
|
1522
1839
|
const nodeCtx = {};
|
|
1523
1840
|
const keyCounts = new Map();
|
|
1524
1841
|
for (const mn of manifest.graph?.nodes || []) {
|
|
1842
|
+
if (mn.kind === 'script') {
|
|
1843
|
+
keyCounts.set(mn.key, (keyCounts.get(mn.key) || 0) + 1);
|
|
1844
|
+
// v4 T5: the SAME builder resolveGraph uses, over the manifest cell's AUTHORED config (the manifest keeps
|
|
1845
|
+
// `config` verbatim) and the LIVE registry entry. No entry => a stub (`file: null`) the preflight refuses.
|
|
1846
|
+
nodeCtx[mn.id] = scriptNodeCtx({ id: mn.id, key: mn.key, config: mn.config }, scr[mn.key]);
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1525
1849
|
if (mn.kind !== 'agent') {
|
|
1526
1850
|
nodeCtx[mn.id] = { nodeId: mn.id, kind: mn.kind, key: null, config: { ...(mn.config || {}) } };
|
|
1527
1851
|
continue;
|
|
@@ -1546,7 +1870,7 @@ export function resolvedFromManifest(manifest, registry) {
|
|
|
1546
1870
|
};
|
|
1547
1871
|
}
|
|
1548
1872
|
for (const nc of Object.values(nodeCtx)) {
|
|
1549
|
-
if (nc.kind
|
|
1873
|
+
if (KEYED_KINDS.includes(nc.kind)) nc.duplicateKey = (keyCounts.get(nc.key) || 0) > 1;
|
|
1550
1874
|
}
|
|
1551
1875
|
const wires = {};
|
|
1552
1876
|
for (const w of manifest.graph?.wires || []) {
|
|
@@ -1559,5 +1883,12 @@ export function resolvedFromManifest(manifest, registry) {
|
|
|
1559
1883
|
agentsByKey[nc.key] = nc.meta;
|
|
1560
1884
|
agentKeys.add(nc.key);
|
|
1561
1885
|
}
|
|
1562
|
-
|
|
1886
|
+
const scriptsByKey = {};
|
|
1887
|
+
const scriptKeys = new Set();
|
|
1888
|
+
for (const nc of Object.values(nodeCtx)) {
|
|
1889
|
+
if (nc.kind !== 'script') continue;
|
|
1890
|
+
scriptsByKey[nc.key] = nc.meta;
|
|
1891
|
+
scriptKeys.add(nc.key);
|
|
1892
|
+
}
|
|
1893
|
+
return { template, ports, loops: classifyLoops(template, ports), nodes: nodeCtx, wires, agentsByKey, agentKeys, scriptsByKey, scriptKeys };
|
|
1563
1894
|
}
|