cool-workflow 0.1.98 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -2
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3534 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/{doctor.js → shell/doctor.js} +123 -56
- package/dist/shell/drive.js +873 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +475 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +98 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +329 -0
- package/dist/{remote-source.js → shell/remote-source.js} +2 -2
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
- package/dist/shell/scheduling-io.js +311 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +471 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +250 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +175 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +280 -219
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +132 -71
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +23 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +2 -2
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1189
- package/dist/capability-registry.js +0 -885
- package/dist/cli/command-surface.js +0 -494
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/ledger.js +0 -169
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/drive.js +0 -864
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -354
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/ledger.js +0 -313
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -470
- package/dist/mcp/tool-definitions.js +0 -1066
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -650
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -925
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -815
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -192
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/pipeline/drive-decide.ts — driveStep/driveConcurrentRound's PURE
|
|
3
|
+
// decision core: task selection, terminal/gate logic, token-budget check,
|
|
4
|
+
// retry/park math, cache-key formulas.
|
|
5
|
+
//
|
|
6
|
+
// MILESTONE 6+7 (combined; the big one — see v2/PLAN.md Open risk 9).
|
|
7
|
+
// Every branch here is a pure function of already-loaded run state; it
|
|
8
|
+
// does not itself spawn a process or touch disk. shell/drive.ts is the
|
|
9
|
+
// thin imperative loop that calls these functions once per step and
|
|
10
|
+
// performs the actual spawn/commit/cache-write IO the decision names.
|
|
11
|
+
//
|
|
12
|
+
// Evidence: SPEC/pipeline-run.md "Drive loop — src/drive.ts", "Drive
|
|
13
|
+
// internals a rebuild must copy", "`--incremental` and the result
|
|
14
|
+
// cache".
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DEFAULT_SCHEDULING_POLICY = exports.MAX_SUB_WORKFLOW_DEPTH = exports.DRIVE_SCHEMA_VERSION = void 0;
|
|
17
|
+
exports.makeStep = makeStep;
|
|
18
|
+
exports.selectDriveTask = selectDriveTask;
|
|
19
|
+
exports.countCompleted = countCompleted;
|
|
20
|
+
exports.countParked = countParked;
|
|
21
|
+
exports.verdictVerifierNodeId = verdictVerifierNodeId;
|
|
22
|
+
exports.exitCodeFromEvidence = exitCodeFromEvidence;
|
|
23
|
+
exports.hasTerminalCommit = hasTerminalCommit;
|
|
24
|
+
exports.terminalOrConfigStep = terminalOrConfigStep;
|
|
25
|
+
exports.retryOrPark = retryOrPark;
|
|
26
|
+
exports.priorAttempts = priorAttempts;
|
|
27
|
+
exports.maxIterations = maxIterations;
|
|
28
|
+
exports.autoWidth = autoWidth;
|
|
29
|
+
exports.roundWidth = roundWidth;
|
|
30
|
+
exports.finalDriveStatus = finalDriveStatus;
|
|
31
|
+
exports.cacheFileName = cacheFileName;
|
|
32
|
+
exports.defaultCacheKey = defaultCacheKey;
|
|
33
|
+
exports.incrementalCacheKey = incrementalCacheKey;
|
|
34
|
+
exports.incrementalDelegationDigest = incrementalDelegationDigest;
|
|
35
|
+
const dispatch_1 = require("./dispatch");
|
|
36
|
+
const hash_1 = require("../hash");
|
|
37
|
+
exports.DRIVE_SCHEMA_VERSION = 1;
|
|
38
|
+
exports.MAX_SUB_WORKFLOW_DEPTH = 4;
|
|
39
|
+
function makeStep(action, status, fields) {
|
|
40
|
+
return { schemaVersion: 1, action, status, ...fields };
|
|
41
|
+
}
|
|
42
|
+
/** The task the next drive step would advance: a running task first,
|
|
43
|
+
* else the next pending task of the first runnable phase. */
|
|
44
|
+
function selectDriveTask(run) {
|
|
45
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
46
|
+
if (!phase)
|
|
47
|
+
return undefined;
|
|
48
|
+
const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
|
|
49
|
+
return phaseTasks.find((task) => task.status === "running") || phaseTasks.find((task) => task.status === "pending");
|
|
50
|
+
}
|
|
51
|
+
function countCompleted(run) {
|
|
52
|
+
return run.tasks.filter((task) => task.status === "completed").length;
|
|
53
|
+
}
|
|
54
|
+
function countParked(run) {
|
|
55
|
+
return run.tasks.filter((task) => task.status === "failed").length;
|
|
56
|
+
}
|
|
57
|
+
/** The completed verdict/synthesis task's verifierNodeId, if any. */
|
|
58
|
+
function verdictVerifierNodeId(run) {
|
|
59
|
+
const verdict = run.tasks.find((task) => /^verdict[:/]|^synthesis[:/]/i.test(task.id) && task.status === "completed");
|
|
60
|
+
return verdict?.verifierNodeId;
|
|
61
|
+
}
|
|
62
|
+
function exitCodeFromEvidence(evidence) {
|
|
63
|
+
const entry = evidence.find((line) => line.startsWith("exitCode:"));
|
|
64
|
+
if (!entry)
|
|
65
|
+
return null;
|
|
66
|
+
const raw = entry.slice("exitCode:".length);
|
|
67
|
+
return raw === "null" ? null : Number(raw);
|
|
68
|
+
}
|
|
69
|
+
/** Whether a commit already exists whose reason starts with
|
|
70
|
+
* "agent-delegation-drive" (the once-only terminal-commit check). */
|
|
71
|
+
function hasTerminalCommit(run) {
|
|
72
|
+
return (run.commits || []).some((commit) => commit.reason && commit.reason.startsWith("agent-delegation-drive"));
|
|
73
|
+
}
|
|
74
|
+
/** terminalOrConfigStep's pure decision half. Returns a DriveStep only
|
|
75
|
+
* for the non-advancing outcomes (terminal commit/complete, blocked
|
|
76
|
+
* phase, blocked token budget, blocked unconfigured agent); returns
|
|
77
|
+
* `{kind: undefined}` when there is a ready task to actually process. */
|
|
78
|
+
function terminalOrConfigStep(run, selected, agentConfigured, tokenBudget) {
|
|
79
|
+
if (!selected) {
|
|
80
|
+
const allComplete = run.tasks.every((task) => task.status === "completed");
|
|
81
|
+
if (allComplete) {
|
|
82
|
+
if (!hasTerminalCommit(run)) {
|
|
83
|
+
return { kind: "commit", verifierNodeId: verdictVerifierNodeId(run) };
|
|
84
|
+
}
|
|
85
|
+
return { kind: "complete", step: makeStep("complete", "complete", { runId: run.id }) };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
kind: "blocked",
|
|
89
|
+
step: makeStep("blocked", "blocked", { runId: run.id, reason: "no eligible worker (a parked/failed worker blocks the phase gate)" }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (tokenBudget && tokenBudget.budget > 0 && tokenBudget.spent >= tokenBudget.budget) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "blocked",
|
|
95
|
+
step: makeStep("blocked", "blocked", {
|
|
96
|
+
runId: run.id,
|
|
97
|
+
taskId: selected.id,
|
|
98
|
+
phase: selected.phase,
|
|
99
|
+
reason: `token budget exhausted: ${tokenBudget.spent} recorded tokens >= budget ${tokenBudget.budget} — refusing to spawn further agents`,
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!agentConfigured) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "blocked",
|
|
106
|
+
step: makeStep("blocked", "blocked", {
|
|
107
|
+
runId: run.id,
|
|
108
|
+
taskId: selected.id,
|
|
109
|
+
phase: selected.phase,
|
|
110
|
+
reason: "agent backend not configured (set CW_AGENT_COMMAND/CW_AGENT_ENDPOINT or pass --agent-command/--agent-endpoint) — refusing rather than fabricating a completion",
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { kind: undefined };
|
|
115
|
+
}
|
|
116
|
+
exports.DEFAULT_SCHEDULING_POLICY = { maxAttempts: 3 };
|
|
117
|
+
/** `retryOrPark` — adds one attempt; at `attempts >= maxAttempts` parks
|
|
118
|
+
* with `parkedReason = "<reason> (attempt <n>/<max>)"`. */
|
|
119
|
+
function retryOrPark(priorAttempts, policy, reason) {
|
|
120
|
+
const attempts = priorAttempts + 1;
|
|
121
|
+
if (attempts >= policy.maxAttempts) {
|
|
122
|
+
return { status: "parked", attempts, parkedReason: `${reason} (attempt ${attempts}/${policy.maxAttempts})` };
|
|
123
|
+
}
|
|
124
|
+
return { status: "retryable", attempts };
|
|
125
|
+
}
|
|
126
|
+
/** `handleHop`'s attempt-accounting rule: prior attempts = max(in-memory
|
|
127
|
+
* count, the worker scope's persisted retryCount). */
|
|
128
|
+
function priorAttempts(inMemoryAttempts, persistedRetryCount) {
|
|
129
|
+
return Math.max(inMemoryAttempts, persistedRetryCount);
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Loop iteration bound.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/** `maxIterations = (plannedWorkers + maxLoopExpansion) * (maxAttempts +
|
|
135
|
+
* 1) + 5`. */
|
|
136
|
+
function maxIterations(plannedWorkers, loopExpansion, policy) {
|
|
137
|
+
return (plannedWorkers + loopExpansion) * (policy.maxAttempts + 1) + 5;
|
|
138
|
+
}
|
|
139
|
+
/** Round width per iteration: an explicit concurrency > 1 wins; else
|
|
140
|
+
* autoWidth for a first-runnable parallel phase. */
|
|
141
|
+
function autoWidth(run) {
|
|
142
|
+
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
143
|
+
if (!phase || phase.mode !== "parallel")
|
|
144
|
+
return 1;
|
|
145
|
+
const cap = Math.max(1, Math.floor(run.workflow.limits?.maxConcurrentAgents || 1));
|
|
146
|
+
return Math.max(1, Math.min(cap, phase.taskIds.length));
|
|
147
|
+
}
|
|
148
|
+
function roundWidth(run, concurrency) {
|
|
149
|
+
return concurrency && concurrency > 1 ? concurrency : autoWidth(run);
|
|
150
|
+
}
|
|
151
|
+
function finalDriveStatus(inputs) {
|
|
152
|
+
if (inputs.once) {
|
|
153
|
+
if (inputs.completedWorkers === inputs.plannedWorkers && inputs.committed)
|
|
154
|
+
return "complete";
|
|
155
|
+
if (inputs.lastStepStatus === "parked" || inputs.lastStepStatus === "blocked")
|
|
156
|
+
return inputs.lastStepStatus;
|
|
157
|
+
return "in-progress";
|
|
158
|
+
}
|
|
159
|
+
if (inputs.exhaustedMaxIterations)
|
|
160
|
+
return "blocked";
|
|
161
|
+
if (inputs.parkedWorkers > 0 || inputs.lastStepStatus === "parked")
|
|
162
|
+
return "parked";
|
|
163
|
+
if (inputs.lastStepStatus === "blocked")
|
|
164
|
+
return "blocked";
|
|
165
|
+
return "complete";
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Result cache key formulas.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** Cache file path (relative to `<run.cwd>/.cw/cache/worker-results/`).
|
|
171
|
+
* Actual path join / atomic write is shell-side. */
|
|
172
|
+
function cacheFileName(taskId, digest) {
|
|
173
|
+
return `${safeFileNamePart(taskId)}-${digest.replace(/^sha256:/, "").slice(0, 32)}.md`;
|
|
174
|
+
}
|
|
175
|
+
/** Kept in sync with shell/fs-atomic.ts's `safeFileName` by the same
|
|
176
|
+
* regex (this pure module cannot import the shell-side impure copy);
|
|
177
|
+
* both are pinned by SPEC/pipeline-run.md's cache-file-path line, which
|
|
178
|
+
* names it `safeFileName` explicitly — the charset MUST include `:` so
|
|
179
|
+
* a task id like `golden:path` keeps its colon in the cache filename. */
|
|
180
|
+
function safeFileNamePart(value) {
|
|
181
|
+
return String(value).replace(/[^a-zA-Z0-9_.:-]+/g, "_");
|
|
182
|
+
}
|
|
183
|
+
/** Default (no `--incremental`) cache key. `undefined` disables caching
|
|
184
|
+
* for this call (no keyInput, empty keyValue, or an unavailable
|
|
185
|
+
* completedResultsDigest upstream). */
|
|
186
|
+
function defaultCacheKey(workflowId, taskId, keyInput, keyValue, promptDigest, completedResultsDigest) {
|
|
187
|
+
if (!keyInput || !keyValue || !keyValue.trim())
|
|
188
|
+
return undefined;
|
|
189
|
+
if (completedResultsDigest === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
return (0, hash_1.sha256)(JSON.stringify({
|
|
192
|
+
schemaVersion: 1,
|
|
193
|
+
workflowId,
|
|
194
|
+
taskId,
|
|
195
|
+
keyInput,
|
|
196
|
+
keyValue: keyValue.trim(),
|
|
197
|
+
promptDigest,
|
|
198
|
+
completedResultsDigest,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
/** `--incremental` cache key (schemaVersion 2 — never collides with
|
|
202
|
+
* schemaVersion 1). `undefined` disables caching (an upstream result is
|
|
203
|
+
* unavailable). */
|
|
204
|
+
function incrementalCacheKey(workflowId, taskId, promptDigest, runInputsDigest, delegationDigest, upstreamResultsDigest) {
|
|
205
|
+
if (upstreamResultsDigest === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
return (0, hash_1.sha256)((0, hash_1.stableStringify)({
|
|
208
|
+
schemaVersion: 2,
|
|
209
|
+
workflowId,
|
|
210
|
+
taskId,
|
|
211
|
+
promptDigest,
|
|
212
|
+
runInputsDigest,
|
|
213
|
+
delegationDigest,
|
|
214
|
+
upstreamResultsDigest,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
/** The delegation digest folded into the incremental cache key. */
|
|
218
|
+
function incrementalDelegationDigest(model, agentType, sandboxProfileId, command, strippedArgs, endpoint) {
|
|
219
|
+
return (0, hash_1.sha256)((0, hash_1.stableStringify)({
|
|
220
|
+
model,
|
|
221
|
+
agentType,
|
|
222
|
+
sandboxProfileId,
|
|
223
|
+
command,
|
|
224
|
+
args: strippedArgs,
|
|
225
|
+
endpoint,
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/pipeline/error-feedback.ts — classifyFeedback, recordFeedback's
|
|
3
|
+
// decision half, dedup key.
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 6+7 (combined). Byte-exact port of the DECISION half of the
|
|
6
|
+
// old build's src/error-feedback.ts (record shape, classification,
|
|
7
|
+
// severity, dedup key, id formatting). The disk write (feedback/*.json +
|
|
8
|
+
// index.json + saveCheckpoint) is the caller's job (shell/), since this
|
|
9
|
+
// file stays pure per the core/shell split.
|
|
10
|
+
//
|
|
11
|
+
// Evidence: SPEC/pipeline-run.md "Error feedback — src/error-feedback.ts".
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ERROR_FEEDBACK_SCHEMA_VERSION = void 0;
|
|
14
|
+
exports.classifyFeedback = classifyFeedback;
|
|
15
|
+
exports.feedbackKey = feedbackKey;
|
|
16
|
+
exports.formatFeedbackId = formatFeedbackId;
|
|
17
|
+
exports.findExistingFeedback = findExistingFeedback;
|
|
18
|
+
exports.buildFeedbackRecord = buildFeedbackRecord;
|
|
19
|
+
exports.summarizeFeedback = summarizeFeedback;
|
|
20
|
+
exports.ERROR_FEEDBACK_SCHEMA_VERSION = 1;
|
|
21
|
+
function normalizeError(error, now) {
|
|
22
|
+
if (typeof error === "string") {
|
|
23
|
+
return { code: "runtime-error", message: error, at: now };
|
|
24
|
+
}
|
|
25
|
+
if (error instanceof Error) {
|
|
26
|
+
return { code: codeFromError(error), message: error.message, at: now };
|
|
27
|
+
}
|
|
28
|
+
return { ...error, code: error.code || "runtime-error", message: error.message || "Unknown error", at: error.at || now };
|
|
29
|
+
}
|
|
30
|
+
function codeFromError(error) {
|
|
31
|
+
if (/Invalid cw:result JSON/i.test(error.message))
|
|
32
|
+
return "result-parse-error";
|
|
33
|
+
if (/requires cw:result evidence/i.test(error.message))
|
|
34
|
+
return "missing-required-evidence";
|
|
35
|
+
if (/requires evidence/i.test(error.message))
|
|
36
|
+
return "missing-required-evidence";
|
|
37
|
+
if (/Phase gate blocked/i.test(error.message))
|
|
38
|
+
return "phase-gate-blocked";
|
|
39
|
+
return "runtime-error";
|
|
40
|
+
}
|
|
41
|
+
/** classifyFeedback — fixed order, byte-exact to the old build. */
|
|
42
|
+
function classifyFeedback(error, context = {}, now = new Date(0).toISOString()) {
|
|
43
|
+
const normalized = normalizeError(error, now);
|
|
44
|
+
const code = normalized.code.toLowerCase();
|
|
45
|
+
if (code.includes("missing-artifact") || code.includes("artifact-path"))
|
|
46
|
+
return "missing-artifact";
|
|
47
|
+
if (code.includes("missing-required-evidence") || code.includes("missing-evidence"))
|
|
48
|
+
return "missing-evidence";
|
|
49
|
+
if (code.includes("verifier") || context.stageId === "verify" || context.source === "verifier")
|
|
50
|
+
return "verifier-failure";
|
|
51
|
+
if (code.includes("illegal-transition") || code.includes("state-transition"))
|
|
52
|
+
return "state-transition";
|
|
53
|
+
if (code.includes("contract") || code.includes("unexpected-node") || context.contractId)
|
|
54
|
+
return "contract-violation";
|
|
55
|
+
if (code.startsWith("sandbox-"))
|
|
56
|
+
return "sandbox-policy";
|
|
57
|
+
if (code.includes("parse") || code.includes("json"))
|
|
58
|
+
return "parse-error";
|
|
59
|
+
if (code.includes("pipeline"))
|
|
60
|
+
return "pipeline-failure";
|
|
61
|
+
if (normalized.code === "runtime-error")
|
|
62
|
+
return "runtime-error";
|
|
63
|
+
return "unknown";
|
|
64
|
+
}
|
|
65
|
+
function severityFor(classification, error) {
|
|
66
|
+
if (classification === "verifier-failure" || classification === "contract-violation")
|
|
67
|
+
return "high";
|
|
68
|
+
if (classification === "sandbox-policy")
|
|
69
|
+
return "medium";
|
|
70
|
+
if (classification === "state-transition" || classification === "missing-evidence")
|
|
71
|
+
return "medium";
|
|
72
|
+
if (classification === "missing-artifact" || classification === "parse-error" || classification === "pipeline-failure") {
|
|
73
|
+
return error.retryable ? "medium" : "low";
|
|
74
|
+
}
|
|
75
|
+
return "low";
|
|
76
|
+
}
|
|
77
|
+
function sourceFor(classification) {
|
|
78
|
+
if (classification === "contract-violation")
|
|
79
|
+
return "contract";
|
|
80
|
+
if (classification === "verifier-failure" || classification === "missing-evidence")
|
|
81
|
+
return "verifier";
|
|
82
|
+
if (classification === "pipeline-failure")
|
|
83
|
+
return "pipeline-runner";
|
|
84
|
+
if (classification === "sandbox-policy")
|
|
85
|
+
return "contract";
|
|
86
|
+
return "manual";
|
|
87
|
+
}
|
|
88
|
+
/** Feedback dedup key: joined with `` (runId, code, message, nodeId,
|
|
89
|
+
* stageId, contractId, path). */
|
|
90
|
+
function feedbackKey(value) {
|
|
91
|
+
return [value.runId || "", value.code || "", value.message || "", value.nodeId || "", value.stageId || "", value.contractId || "", value.path || ""].join("");
|
|
92
|
+
}
|
|
93
|
+
function compactMetadata(metadata) {
|
|
94
|
+
const compacted = {};
|
|
95
|
+
for (const [key, value] of Object.entries(metadata))
|
|
96
|
+
if (value !== undefined)
|
|
97
|
+
compacted[key] = value;
|
|
98
|
+
return Object.keys(compacted).length ? compacted : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Deterministic feedback id: position in the run's append-only feedback
|
|
101
|
+
* log (1-based), qualified by classification. */
|
|
102
|
+
function formatFeedbackId(classification, seq) {
|
|
103
|
+
return `feedback-${classification}-${String(seq).padStart(4, "0")}`;
|
|
104
|
+
}
|
|
105
|
+
/** Find an existing UNRESOLVED record matching the same
|
|
106
|
+
* {code,message,nodeId,stageId,contractId,path} — recordFeedback's dedup
|
|
107
|
+
* rule. */
|
|
108
|
+
function findExistingFeedback(existing, error, nodeId, stageId, contractId, path) {
|
|
109
|
+
return existing.find((record) => record.status !== "resolved" &&
|
|
110
|
+
record.code === error.code &&
|
|
111
|
+
record.message === error.message &&
|
|
112
|
+
record.nodeId === nodeId &&
|
|
113
|
+
record.stageId === stageId &&
|
|
114
|
+
record.contractId === contractId &&
|
|
115
|
+
record.path === path);
|
|
116
|
+
}
|
|
117
|
+
/** Build a new (not-yet-persisted) feedback record. `existingCount` is
|
|
118
|
+
* `(run.feedback || []).length` BEFORE this record is appended. */
|
|
119
|
+
function buildFeedbackRecord(runId, input, existingCount, now) {
|
|
120
|
+
const error = normalizeError(input.error, now);
|
|
121
|
+
const nodeId = input.nodeId || error.nodeId;
|
|
122
|
+
const classification = classifyFeedback(error, { source: input.source, stageId: input.stageId, contractId: input.contractId, metadata: input.metadata }, now);
|
|
123
|
+
return {
|
|
124
|
+
schemaVersion: exports.ERROR_FEEDBACK_SCHEMA_VERSION,
|
|
125
|
+
id: formatFeedbackId(classification, existingCount + 1),
|
|
126
|
+
runId,
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
status: "open",
|
|
130
|
+
severity: severityFor(classification, error),
|
|
131
|
+
classification,
|
|
132
|
+
source: input.source || sourceFor(classification),
|
|
133
|
+
code: error.code,
|
|
134
|
+
message: error.message,
|
|
135
|
+
nodeId,
|
|
136
|
+
stageId: input.stageId,
|
|
137
|
+
contractId: input.contractId,
|
|
138
|
+
taskId: input.taskId,
|
|
139
|
+
path: input.path || error.path,
|
|
140
|
+
retryable: input.retryable ?? error.retryable ?? false,
|
|
141
|
+
evidence: input.evidence || [],
|
|
142
|
+
artifacts: input.artifacts || [],
|
|
143
|
+
metadata: compactMetadata({ ...input.metadata, details: input.metadata?.details || error.details }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function summarizeFeedback(records) {
|
|
147
|
+
return {
|
|
148
|
+
total: records.length,
|
|
149
|
+
byStatus: countBy(records, (r) => r.status),
|
|
150
|
+
bySeverity: countBy(records, (r) => r.severity),
|
|
151
|
+
byClassification: countBy(records, (r) => r.classification),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function countBy(values, key) {
|
|
155
|
+
const counts = {};
|
|
156
|
+
for (const value of values) {
|
|
157
|
+
const bucket = key(value);
|
|
158
|
+
counts[bucket] = (counts[bucket] || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
return counts;
|
|
161
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/pipeline/loop-expansion.ts — predicate registry, maxLoopExpansion,
|
|
3
|
+
// maybeExpandLoop's decision half (round clone, stop reasons).
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 6+7 (combined). Byte-exact port of the old build's
|
|
6
|
+
// src/loop-expansion.ts (predicate registry + maxLoopExpansion are
|
|
7
|
+
// already pure there) plus the DECISION half of
|
|
8
|
+
// src/orchestrator/lifecycle-operations.ts's maybeExpandLoop (materializing
|
|
9
|
+
// the cloned phase/tasks + loop-control node is the caller's job in
|
|
10
|
+
// shell/, since it needs writeTaskFiles + the plan pipeline stage).
|
|
11
|
+
//
|
|
12
|
+
// Evidence: SPEC/pipeline-run.md "loop() expansion — src/loop-
|
|
13
|
+
// expansion.ts + maybeExpandLoop".
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.registerLoopPredicate = registerLoopPredicate;
|
|
16
|
+
exports.getLoopPredicate = getLoopPredicate;
|
|
17
|
+
exports.hasLoopPredicate = hasLoopPredicate;
|
|
18
|
+
exports.maxLoopExpansion = maxLoopExpansion;
|
|
19
|
+
exports.evaluateLoopStop = evaluateLoopStop;
|
|
20
|
+
exports.cloneLoopRoundTasks = cloneLoopRoundTasks;
|
|
21
|
+
exports.loopControlNodeId = loopControlNodeId;
|
|
22
|
+
const REGISTRY = new Map();
|
|
23
|
+
function registerLoopPredicate(name, fn) {
|
|
24
|
+
REGISTRY.set(name, fn);
|
|
25
|
+
}
|
|
26
|
+
function getLoopPredicate(name) {
|
|
27
|
+
return REGISTRY.get(name);
|
|
28
|
+
}
|
|
29
|
+
function hasLoopPredicate(name) {
|
|
30
|
+
return REGISTRY.has(name);
|
|
31
|
+
}
|
|
32
|
+
registerLoopPredicate("no-new-findings", (ctx) => {
|
|
33
|
+
const empty = ctx.roundResults.every((r) => !r || !Array.isArray(r.findings) || r.findings.length === 0);
|
|
34
|
+
return empty
|
|
35
|
+
? { done: true, reason: "no-new-findings: the latest round produced no findings" }
|
|
36
|
+
: { done: false, reason: "no-new-findings: the latest round still has findings" };
|
|
37
|
+
});
|
|
38
|
+
registerLoopPredicate("single-round", () => ({ done: true, reason: "single-round: stop after one round" }));
|
|
39
|
+
/** Static worst-case number of EXTRA tasks a fully-expanded run could
|
|
40
|
+
* mint, derived purely from the workflow declaration. Zero with no loop
|
|
41
|
+
* phases. */
|
|
42
|
+
function maxLoopExpansion(run) {
|
|
43
|
+
let extra = 0;
|
|
44
|
+
for (const phase of run.phases) {
|
|
45
|
+
const loop = phase.loop;
|
|
46
|
+
if (loop && typeof loop.maxRounds === "number" && loop.maxRounds > 1) {
|
|
47
|
+
extra += (loop.maxRounds - 1) * phase.taskIds.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return extra;
|
|
51
|
+
}
|
|
52
|
+
/** Decide whether a just-completed loop round should stop, given the
|
|
53
|
+
* origin phase's loop spec and the round's recorded results. Pure —
|
|
54
|
+
* `until:{kind:"predicate"}` looks up the registry; an unregistered ref
|
|
55
|
+
* stops fail-closed (never throws). */
|
|
56
|
+
function evaluateLoopStop(origin, round, ctx) {
|
|
57
|
+
const loop = origin.loop;
|
|
58
|
+
if (!loop)
|
|
59
|
+
return { done: true, atCap: false, reason: "no loop spec" };
|
|
60
|
+
const maxRounds = loop.maxRounds;
|
|
61
|
+
let decision;
|
|
62
|
+
if (loop.until.kind === "budget-target") {
|
|
63
|
+
const target = loop.until.target || 0;
|
|
64
|
+
const spent = ctx.usageTotals.totalTokens;
|
|
65
|
+
decision = spent >= target
|
|
66
|
+
? { done: true, reason: `budget-target: ${spent}/${target} recorded tokens` }
|
|
67
|
+
: { done: false, reason: `budget-target: ${spent}/${target} recorded tokens` };
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const ref = loop.until.ref || "";
|
|
71
|
+
const predicate = getLoopPredicate(ref);
|
|
72
|
+
decision = predicate ? predicate(ctx) : { done: true, reason: `loop predicate "${ref}" not registered — stopping fail-closed` };
|
|
73
|
+
}
|
|
74
|
+
const atCap = round >= maxRounds;
|
|
75
|
+
return { done: decision.done || atCap, atCap, reason: decision.reason };
|
|
76
|
+
}
|
|
77
|
+
/** Clone round-1 template tasks into `<base-id>@r<nextRound>` for a fresh
|
|
78
|
+
* phase `id: <origin-id>@r<nextRound>`, `name: <origin name> (round
|
|
79
|
+
* <n>)`. Pure data transform; the caller writes task files + runs the
|
|
80
|
+
* plan pipeline stage per new task. */
|
|
81
|
+
function cloneLoopRoundTasks(origin, templateTasks, nextRound) {
|
|
82
|
+
// Round-suffix strips any prior @rN so round 3 clones round 1's base id,
|
|
83
|
+
// never round 2's suffixed id — byte-exact to the old build's
|
|
84
|
+
// `t.id.replace(/@r\d+$/, "")` in maybeExpandLoop.
|
|
85
|
+
const suffix = `@r${nextRound}`;
|
|
86
|
+
const nextPhaseName = `${origin.name} (round ${nextRound})`;
|
|
87
|
+
const tasks = templateTasks.map((task) => ({
|
|
88
|
+
...task,
|
|
89
|
+
id: `${task.id.replace(/@r\d+$/, "")}${suffix}`,
|
|
90
|
+
// The cloned tasks belong to the NEW round phase, not round 1 — the old
|
|
91
|
+
// build set `phase: nextPhaseName` on every cloned task so progress views
|
|
92
|
+
// and previousPhaseResultsDigest resolve to the right phase.
|
|
93
|
+
phase: nextPhaseName,
|
|
94
|
+
status: "pending",
|
|
95
|
+
loopStage: "interpret",
|
|
96
|
+
loopRound: nextRound,
|
|
97
|
+
dispatchId: undefined,
|
|
98
|
+
dispatchedAt: undefined,
|
|
99
|
+
startedAt: undefined,
|
|
100
|
+
completedAt: undefined,
|
|
101
|
+
result: undefined,
|
|
102
|
+
stateNodeId: undefined,
|
|
103
|
+
resultNodeId: undefined,
|
|
104
|
+
verifierNodeId: undefined,
|
|
105
|
+
workerId: undefined,
|
|
106
|
+
workerManifestPath: undefined,
|
|
107
|
+
taskPath: "",
|
|
108
|
+
resultPath: "",
|
|
109
|
+
}));
|
|
110
|
+
const phase = {
|
|
111
|
+
id: `${origin.id}${suffix}`,
|
|
112
|
+
name: nextPhaseName,
|
|
113
|
+
status: "pending",
|
|
114
|
+
taskIds: tasks.map((t) => t.id),
|
|
115
|
+
...(origin.mode ? { mode: origin.mode } : {}),
|
|
116
|
+
loopOrigin: origin.id,
|
|
117
|
+
loopRound: nextRound,
|
|
118
|
+
};
|
|
119
|
+
return { phase, tasks };
|
|
120
|
+
}
|
|
121
|
+
/** Loop-control node id: `<run-id>:loop-control:<origin-phase-id>:r<round>`. */
|
|
122
|
+
function loopControlNodeId(runId, originPhaseId, round) {
|
|
123
|
+
return `${runId}:loop-control:${originPhaseId}:r${round}`;
|
|
124
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// core/pipeline/result-normalize.ts — normalizeResultEnvelope,
|
|
3
|
+
// isEmptyCapture.
|
|
4
|
+
//
|
|
5
|
+
// MILESTONE 6+7 (combined). Byte-exact port of the old build's
|
|
6
|
+
// src/result-normalize.ts. Pure (no fs/clock/random).
|
|
7
|
+
//
|
|
8
|
+
// Evidence: SPEC/pipeline-run.md "Result ingest — src/result-normalize.ts".
|
|
2
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
10
|
exports.normalizeResultEnvelope = normalizeResultEnvelope;
|
|
4
11
|
exports.firstNonEmptyLine = firstNonEmptyLine;
|
|
5
12
|
exports.isEmptyCapture = isEmptyCapture;
|
|
6
|
-
const evidence_grounding_1 = require("
|
|
7
|
-
// Alternative keys a capable model naturally uses for the findings array.
|
|
13
|
+
const evidence_grounding_1 = require("../trust/evidence-grounding");
|
|
8
14
|
const FINDING_ARRAY_KEYS = [
|
|
9
15
|
"findings",
|
|
10
16
|
"candidate_risks",
|
|
@@ -16,22 +22,15 @@ const FINDING_ARRAY_KEYS = [
|
|
|
16
22
|
"topRisks",
|
|
17
23
|
"issues",
|
|
18
24
|
"problems",
|
|
19
|
-
"concerns"
|
|
25
|
+
"concerns",
|
|
20
26
|
];
|
|
21
|
-
// Per-finding keys that may carry evidence locators.
|
|
22
27
|
const FINDING_EVIDENCE_KEYS = ["evidence", "evidence_paths", "evidencePaths", "locators", "refs", "files", "location", "locations", "path", "paths", "line", "lines", "where"];
|
|
23
28
|
const CLASSIFICATIONS = new Set(["real", "conditional", "non-issue", "unknown"]);
|
|
24
|
-
// Evidence caps (v0.1.42): the v0.1.41 experiment hit a single flat cap of 64 on
|
|
25
|
-
// the top-level union, silently dropping locators on richly-cited workers. The
|
|
26
|
-
// top-level array now holds the whole run's harvested locators (high cap), while
|
|
27
|
-
// each finding keeps a focused, readable set.
|
|
28
29
|
const TOP_LEVEL_EVIDENCE_CAP = 256;
|
|
29
30
|
const PER_FINDING_EVIDENCE_CAP = 32;
|
|
30
31
|
function normalizeResultEnvelope(markdown) {
|
|
31
32
|
const match = markdown.match(/```cw:result\s*([\s\S]*?)```/);
|
|
32
33
|
if (!match) {
|
|
33
|
-
// No fence: still DERIVE grounded evidence from the prose so a fence-less but
|
|
34
|
-
// well-cited report is not silently captured as empty.
|
|
35
34
|
return { summary: firstNonEmptyLine(markdown), findings: [], evidence: harvestGrounded([markdown], TOP_LEVEL_EVIDENCE_CAP) };
|
|
36
35
|
}
|
|
37
36
|
let parsed;
|
|
@@ -43,27 +42,17 @@ function normalizeResultEnvelope(markdown) {
|
|
|
43
42
|
throw new Error(`Invalid cw:result JSON: ${message}`);
|
|
44
43
|
}
|
|
45
44
|
const summary = pickString(parsed, ["summary", "short_answer", "shortAnswer", "verdict", "answer", "conclusion"]) || firstNonEmptyLine(markdown);
|
|
46
|
-
// Findings: prefer the canonical `findings` array, else the first present
|
|
47
|
-
// alternative array. EVERY finding is then normalized (id guaranteed, severity/
|
|
48
|
-
// classification coerced, evidence derived) so a capable agent that omits an `id`
|
|
49
|
-
// or uses `high`/`candidate_risks` is captured rather than fail-closed-rejected.
|
|
50
45
|
const canonicalFindings = Array.isArray(parsed.findings) ? parsed.findings : undefined;
|
|
51
46
|
const rawFindings = (canonicalFindings && canonicalFindings.length ? canonicalFindings : extractFindingsRaw(parsed)) ?? [];
|
|
52
47
|
const findings = rawFindings.map((item, index) => normalizeFinding(item, index));
|
|
53
|
-
// Evidence: canonical non-empty wins; else CW derives grounded locators itself
|
|
54
|
-
// from the entire envelope JSON, the findings, and the prose.
|
|
55
48
|
const canonicalEvidence = Array.isArray(parsed.evidence) ? parsed.evidence : undefined;
|
|
56
|
-
const evidence = canonicalEvidence && canonicalEvidence.length
|
|
57
|
-
? canonicalEvidence
|
|
58
|
-
: harvestGrounded([parsed, findings, stripFence(markdown)], TOP_LEVEL_EVIDENCE_CAP);
|
|
49
|
+
const evidence = canonicalEvidence && canonicalEvidence.length ? canonicalEvidence : harvestGrounded([parsed, findings, stripFence(markdown)], TOP_LEVEL_EVIDENCE_CAP);
|
|
59
50
|
return { summary, findings, evidence };
|
|
60
51
|
}
|
|
61
|
-
/** The first recognized alternative findings array (raw items, normalized by the
|
|
62
|
-
* caller). Returns undefined when no alternative findings source is present. */
|
|
63
52
|
function extractFindingsRaw(parsed) {
|
|
64
53
|
for (const key of FINDING_ARRAY_KEYS) {
|
|
65
54
|
if (key === "findings")
|
|
66
|
-
continue;
|
|
55
|
+
continue;
|
|
67
56
|
const raw = parsed[key];
|
|
68
57
|
if (Array.isArray(raw) && raw.length)
|
|
69
58
|
return raw;
|
|
@@ -76,19 +65,16 @@ function normalizeFinding(item, index) {
|
|
|
76
65
|
const id = pickString(obj, ["id", "key", "name", "title"]) || `finding-${index + 1}`;
|
|
77
66
|
const classification = normalizeClassification(pickString(obj, ["classification", "class", "kind", "status"]));
|
|
78
67
|
const severity = normalizeSeverity(pickString(obj, ["severity", "priority", "level", "rank", "rating"]));
|
|
79
|
-
// Evidence: explicit per-finding evidence keys first, else derive grounded
|
|
80
|
-
// locators from the whole finding object (so `{title, detail, file, line}` works).
|
|
81
68
|
const explicit = harvestGrounded(FINDING_EVIDENCE_KEYS.map((k) => obj[k]).filter((v) => v !== undefined), PER_FINDING_EVIDENCE_CAP);
|
|
82
69
|
const evidence = explicit.length ? explicit : harvestGrounded([obj], PER_FINDING_EVIDENCE_CAP);
|
|
83
|
-
return { id, classification, severity, evidence };
|
|
70
|
+
return { id, classification: classification ?? "unknown", severity, evidence };
|
|
84
71
|
}
|
|
85
|
-
// A bare string finding ("P1 — revoke stays live (app.ts:1644)").
|
|
86
72
|
const text = String(item ?? "");
|
|
87
73
|
return {
|
|
88
74
|
id: `finding-${index + 1}`,
|
|
89
75
|
classification: "unknown",
|
|
90
76
|
severity: normalizeSeverity(text),
|
|
91
|
-
evidence: harvestGrounded([text], PER_FINDING_EVIDENCE_CAP)
|
|
77
|
+
evidence: harvestGrounded([text], PER_FINDING_EVIDENCE_CAP),
|
|
92
78
|
};
|
|
93
79
|
}
|
|
94
80
|
function normalizeClassification(value) {
|
|
@@ -120,9 +106,6 @@ function normalizeSeverity(value) {
|
|
|
120
106
|
return "P3";
|
|
121
107
|
return "none";
|
|
122
108
|
}
|
|
123
|
-
/** Collect GROUNDED locators (path:line / URL / namespace:value) from any mix of
|
|
124
|
-
* JSON values and prose strings. This is how CW derives evidence itself instead
|
|
125
|
-
* of trusting the agent's `evidence` key. Deterministic: deduped + sorted + capped. */
|
|
126
109
|
function harvestGrounded(values, cap = TOP_LEVEL_EVIDENCE_CAP) {
|
|
127
110
|
const acc = [];
|
|
128
111
|
for (const v of values)
|
|
@@ -136,9 +119,6 @@ function collect(value, acc) {
|
|
|
136
119
|
return;
|
|
137
120
|
if (typeof value === "string") {
|
|
138
121
|
const v = value.trim();
|
|
139
|
-
// A single whitespace-free token that is itself a grounded locator is kept
|
|
140
|
-
// verbatim; anything with spaces is PROSE — extract the locator tokens from it
|
|
141
|
-
// (a sentence containing a "/" must not be captured whole as one "locator").
|
|
142
122
|
if (v && !/\s/.test(v) && (0, evidence_grounding_1.isGroundedEvidence)(v))
|
|
143
123
|
acc.push(v);
|
|
144
124
|
else
|
|
@@ -156,8 +136,6 @@ function collect(value, acc) {
|
|
|
156
136
|
collect(item, acc);
|
|
157
137
|
}
|
|
158
138
|
}
|
|
159
|
-
// Pull file:line / file.ext / URL / `backtick` tokens out of a prose string, then
|
|
160
|
-
// keep only the grounded ones. Backtick spans are a strong signal for code refs.
|
|
161
139
|
const LOCATOR_RE = /`([^`]+)`|([A-Za-z0-9_@./-]+\.[A-Za-z]{1,8}(?::\d+(?:-\d+)?)?)|(https?:\/\/[^\s)]+)/g;
|
|
162
140
|
function locatorsFromText(text) {
|
|
163
141
|
const out = [];
|
|
@@ -187,8 +165,7 @@ function firstNonEmptyLine(markdown) {
|
|
|
187
165
|
.map((line) => line.trim())
|
|
188
166
|
.find((line) => line && !line.startsWith("#") && !line.startsWith("```")) || "");
|
|
189
167
|
}
|
|
190
|
-
/** True when an accepted result captured no structured signal at all.
|
|
191
|
-
* "warn, don't silently pass" surface in the record paths. */
|
|
168
|
+
/** True when an accepted result captured no structured signal at all. */
|
|
192
169
|
function isEmptyCapture(envelope) {
|
|
193
170
|
return (envelope.findings?.length || 0) === 0 && (envelope.evidence?.length || 0) === 0;
|
|
194
171
|
}
|