@duckmind/dm-windows-x64 0.61.4 → 0.61.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dm.exe +0 -0
- package/extensions/.dm-extensions.json +211 -67
- package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
- package/extensions/dm-subagents/agents/claude-code.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec.md +15 -0
- package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
- package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
- package/extensions/dm-subagents/agents/delegate.md +3 -2
- package/extensions/dm-subagents/agents/oracle.md +10 -5
- package/extensions/dm-subagents/agents/researcher.md +2 -2
- package/extensions/dm-subagents/agents/reviewer.md +17 -7
- package/extensions/dm-subagents/agents/scout.md +5 -5
- package/extensions/dm-subagents/agents/worker.md +6 -2
- package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
- package/extensions/dm-subagents/index.js +4 -0
- package/extensions/dm-subagents/inspector-runner.mjs +10 -0
- package/extensions/dm-subagents/install.mjs +3 -2
- package/extensions/dm-subagents/package.json +2 -2
- package/extensions/dm-subagents/prompts/council.md +60 -0
- package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
- package/extensions/dm-subagents/prompts/review-loop.md +13 -7
- package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
- package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
- package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
- package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
- package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
- package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
- package/extensions/dm-subagents/src/agents/agents.js +1447 -299
- package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
- package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
- package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
- package/extensions/dm-subagents/src/agents/identity.js +1 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
- package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
- package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
- package/extensions/dm-subagents/src/agents/skills.js +52 -35
- package/extensions/dm-subagents/src/api/agents.js +6 -0
- package/extensions/dm-subagents/src/api/background-work.js +151 -0
- package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
- package/extensions/dm-subagents/src/api/control-channel.js +3 -0
- package/extensions/dm-subagents/src/api/delegation.js +5 -0
- package/extensions/dm-subagents/src/api/dm-args.js +3 -0
- package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
- package/extensions/dm-subagents/src/api/external-runs.js +233 -0
- package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
- package/extensions/dm-subagents/src/api/preflight.js +322 -0
- package/extensions/dm-subagents/src/api/project-panes.js +11 -0
- package/extensions/dm-subagents/src/api/shared-types.js +4 -0
- package/extensions/dm-subagents/src/extension/config.js +175 -0
- package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
- package/extensions/dm-subagents/src/extension/doctor.js +71 -17
- package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
- package/extensions/dm-subagents/src/extension/index.js +711 -265
- package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
- package/extensions/dm-subagents/src/extension/rpc.js +427 -22
- package/extensions/dm-subagents/src/extension/schemas.js +158 -65
- package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
- package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
- package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
- package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
- package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
- package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
- package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
- package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
- package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
- package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
- package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
- package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
- package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
- package/extensions/dm-subagents/src/missions/actions.js +394 -0
- package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
- package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
- package/extensions/dm-subagents/src/missions/store.js +548 -0
- package/extensions/dm-subagents/src/missions/types.js +9 -0
- package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
- package/extensions/dm-subagents/src/policy/authority.js +37 -0
- package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
- package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
- package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
- package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
- package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
- package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
- package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
- package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
- package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
- package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
- package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
- package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
- package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
- package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
- package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
- package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
- package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
- package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
- package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
- package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
- package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
- package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
- package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
- package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
- package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
- package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
- package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
- package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
- package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
- package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
- package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
- package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
- package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
- package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
- package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
- package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
- package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
- package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
- package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
- package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
- package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
- package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
- package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
- package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
- package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
- package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
- package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
- package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
- package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
- package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
- package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
- package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
- package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
- package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
- package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
- package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
- package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
- package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
- package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
- package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
- package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
- package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
- package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
- package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
- package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
- package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
- package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
- package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
- package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
- package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
- package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
- package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
- package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
- package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
- package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
- package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
- package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
- package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
- package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
- package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
- package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
- package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
- package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
- package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
- package/extensions/dm-subagents/src/shared/display-text.js +142 -0
- package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
- package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
- package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
- package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
- package/extensions/dm-subagents/src/shared/formatters.js +21 -7
- package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
- package/extensions/dm-subagents/src/shared/model-info.js +10 -5
- package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
- package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
- package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
- package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
- package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
- package/extensions/dm-subagents/src/shared/settings.js +39 -47
- package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
- package/extensions/dm-subagents/src/shared/status-format.js +9 -2
- package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
- package/extensions/dm-subagents/src/shared/types.js +47 -7
- package/extensions/dm-subagents/src/shared/utf8.js +12 -0
- package/extensions/dm-subagents/src/shared/utils.js +151 -131
- package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
- package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
- package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
- package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
- package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
- package/extensions/dm-subagents/src/slash/selector.js +101 -0
- package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
- package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
- package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
- package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
- package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
- package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
- package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
- package/extensions/dm-subagents/src/tui/render.js +1511 -261
- package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
- package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
- package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
- package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
- package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
- package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
- package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
- package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
- package/extensions/dm-subagents/src/watchdog/render.js +54 -0
- package/extensions/dm-subagents/src/watchdog/review.js +251 -0
- package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
- package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
- package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
- package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
- package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
- package/extensions/dm-subagents/src/watchdog/types.js +29 -0
- package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
- package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
- package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
- package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
- package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
- package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
- package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
- package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
- package/package.json +4 -3
- package/extensions/dm-fff/package.json +0 -21
- package/extensions/dm-fff/src/index.js +0 -691
- package/extensions/dm-fff/src/query.js +0 -60
- package/extensions/dm-subagents/agents/context-builder.md +0 -46
- package/extensions/dm-subagents/agents/planner.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import * as fs from "node:fs";
|
|
3
5
|
import * as path from "node:path";
|
|
6
|
+
import { isAgentContractV1 } from "./agent-contract.js";
|
|
7
|
+
import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.js";
|
|
4
8
|
const LEVEL_RANK = {
|
|
5
9
|
none: 0,
|
|
6
10
|
attested: 1,
|
|
7
11
|
checked: 2,
|
|
8
|
-
verified: 3
|
|
9
|
-
reviewed: 4
|
|
12
|
+
verified: 3
|
|
10
13
|
};
|
|
11
|
-
const VALID_LEVELS = new Set(["auto", "none", "attested", "checked", "verified"
|
|
12
|
-
const
|
|
14
|
+
const VALID_LEVELS = new Set(["auto", "none", "attested", "checked", "verified"]);
|
|
15
|
+
const VALID_EVIDENCE_KINDS = [
|
|
13
16
|
"changed-files",
|
|
14
17
|
"tests-added",
|
|
15
18
|
"commands-run",
|
|
@@ -19,11 +22,15 @@ const VALID_EVIDENCE = new Set([
|
|
|
19
22
|
"diff-summary",
|
|
20
23
|
"review-findings",
|
|
21
24
|
"manual-notes"
|
|
22
|
-
]
|
|
25
|
+
];
|
|
26
|
+
const VALID_EVIDENCE = new Set(VALID_EVIDENCE_KINDS);
|
|
27
|
+
const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
|
|
28
|
+
const ACCEPTANCE_OBJECT_EXAMPLE = 'Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.';
|
|
23
29
|
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
24
30
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
25
31
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
26
32
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
33
|
+
const EXPLICIT_REVIEWED_UNAVAILABLE = "is an achieved status, not a requestable acceptance level. For a read-only reviewer call, omit acceptance. To require independent review of a writer result, use acceptance.review.required and orchestrate the reviewer separately.";
|
|
27
34
|
function normalizeLevel(level) {
|
|
28
35
|
return level ?? "auto";
|
|
29
36
|
}
|
|
@@ -39,7 +46,6 @@ function requiredEvidenceForLevel(level) {
|
|
|
39
46
|
case "checked":
|
|
40
47
|
return ["changed-files", "tests-added", "commands-run", "residual-risks", "no-staged-files"];
|
|
41
48
|
case "verified":
|
|
42
|
-
case "reviewed":
|
|
43
49
|
return ["changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files"];
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -47,24 +53,30 @@ function inferLevel(input) {
|
|
|
47
53
|
const agent = input.agentName.toLowerCase();
|
|
48
54
|
const task = input.task?.toLowerCase() ?? "";
|
|
49
55
|
const reasons = [];
|
|
50
|
-
const
|
|
51
|
-
const readOnlyTask = /\b(?:read[- ]only|review[- ]only|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
56
|
+
const intent = classifyTaskMutationIntent(input.acceptanceRole ? "worker" : input.agentName, input.task ?? "");
|
|
57
|
+
const readOnlyTask = intent.kind === "read-only" || intent.kind === "unknown" && /\b(?:read[- ]only|review[- ]only|no edits|without edits|inspect|summari[sz]e)\b/.test(task);
|
|
58
|
+
const rolePatchTask = input.acceptanceRole !== undefined && intent.kind !== "read-only" && !/\b(?:do not|don't|must not)\s+patch\b/.test(task) && /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(stripSeverityCompounds(task));
|
|
59
|
+
const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
60
|
+
const readOnlyAgent = input.acceptanceRole === "read-only" || input.acceptanceRole === undefined && /\b(?:reviewer|oracle|scout|researcher|analyst)\b/.test(agent);
|
|
61
|
+
const writeTask = taskMayWrite || input.acceptanceRole === "writer" && !readOnlyTask || input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask;
|
|
62
|
+
const inferredReadOnly = readOnlyTask || input.acceptanceRole === "read-only" && !taskMayWrite;
|
|
63
|
+
const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
|
|
64
|
+
const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
|
|
65
|
+
const risky = Boolean(input.async && writeTask) || Boolean(input.dynamic) && !roleResolvesReadOnly || Boolean(input.dynamicGroup) && !roleResolvesReadOnly || !keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
|
|
54
66
|
if (risky) {
|
|
55
67
|
reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
|
|
56
68
|
if (input.dynamic || input.dynamicGroup)
|
|
57
69
|
reasons.push("dynamic fanout context");
|
|
58
70
|
return {
|
|
59
|
-
level: "
|
|
71
|
+
level: "checked",
|
|
60
72
|
reasons,
|
|
61
73
|
criteria: ["Implement the requested change without widening scope", "Return evidence sufficient for an independent acceptance review"],
|
|
62
|
-
evidence: requiredEvidenceForLevel("
|
|
74
|
+
evidence: requiredEvidenceForLevel("checked"),
|
|
63
75
|
review: { agent: "reviewer", required: true }
|
|
64
76
|
};
|
|
65
77
|
}
|
|
66
78
|
if (writeTask && !readOnlyTask) {
|
|
67
|
-
reasons.push("write-capable worker/task");
|
|
79
|
+
reasons.push(input.acceptanceRole === "writer" && !taskMayWrite ? "declared writer acceptance role" : "write-capable worker/task");
|
|
68
80
|
return {
|
|
69
81
|
level: "checked",
|
|
70
82
|
reasons,
|
|
@@ -73,7 +85,7 @@ function inferLevel(input) {
|
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
if (readOnlyAgent || readOnlyTask) {
|
|
76
|
-
reasons.push(readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
88
|
+
reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
77
89
|
return {
|
|
78
90
|
level: "attested",
|
|
79
91
|
reasons,
|
|
@@ -98,9 +110,22 @@ export function normalizeAcceptanceInput(input) {
|
|
|
98
110
|
return { level: input };
|
|
99
111
|
return { ...input };
|
|
100
112
|
}
|
|
113
|
+
export function normalizeGateAcceptance(gate, acceptance) {
|
|
114
|
+
if (gate === undefined)
|
|
115
|
+
return acceptance === undefined ? { ok: true } : { ok: true, acceptance };
|
|
116
|
+
if (typeof gate !== "string" || !gate.trim())
|
|
117
|
+
return { ok: false, error: "gate must be a non-empty command string." };
|
|
118
|
+
if (acceptance !== undefined)
|
|
119
|
+
return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
120
|
+
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
121
|
+
}
|
|
101
122
|
function explicitAcceptanceCanDisable(explicit) {
|
|
102
123
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
103
124
|
}
|
|
125
|
+
function unsupportedEvidenceKindMessage(pathLabel, item) {
|
|
126
|
+
const value = typeof item === "string" ? ` "${item}"` : "";
|
|
127
|
+
return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
|
|
128
|
+
}
|
|
104
129
|
export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
105
130
|
const errors = [];
|
|
106
131
|
if (input === undefined)
|
|
@@ -108,12 +133,18 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
108
133
|
if (input === false)
|
|
109
134
|
return errors;
|
|
110
135
|
if (typeof input === "string") {
|
|
111
|
-
if (
|
|
136
|
+
if (input === "reviewed")
|
|
137
|
+
errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
138
|
+
else if (!VALID_LEVELS.has(input))
|
|
112
139
|
errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
140
|
+
else if (input === "none")
|
|
141
|
+
errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
142
|
+
else if (input === "verified")
|
|
143
|
+
errors.push(`${pathLabel} level "verified" requires object form with at least one runtime verify command. Use level "checked" or provide a non-empty acceptance.verify array.`);
|
|
113
144
|
return errors;
|
|
114
145
|
}
|
|
115
146
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
116
|
-
errors.push(`${pathLabel} must be a string level, false, or an object
|
|
147
|
+
errors.push(`${pathLabel} must be a string level, false, or an object. ${ACCEPTANCE_OBJECT_EXAMPLE}`);
|
|
117
148
|
return errors;
|
|
118
149
|
}
|
|
119
150
|
const value = input;
|
|
@@ -121,8 +152,10 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
121
152
|
if (!ACCEPTANCE_CONFIG_KEYS.has(key))
|
|
122
153
|
errors.push(`${pathLabel}.${key} is not supported.`);
|
|
123
154
|
}
|
|
124
|
-
if (value.level
|
|
125
|
-
errors.push(`${pathLabel}.level
|
|
155
|
+
if (value.level === "reviewed") {
|
|
156
|
+
errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
157
|
+
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level))) {
|
|
158
|
+
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
126
159
|
}
|
|
127
160
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
128
161
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
@@ -132,6 +165,7 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
132
165
|
if (value.criteria !== undefined && !Array.isArray(value.criteria))
|
|
133
166
|
errors.push(`${pathLabel}.criteria must be an array.`);
|
|
134
167
|
if (Array.isArray(value.criteria)) {
|
|
168
|
+
const criterionIds = new Set;
|
|
135
169
|
for (const [index, criterion] of value.criteria.entries()) {
|
|
136
170
|
if (typeof criterion === "string")
|
|
137
171
|
continue;
|
|
@@ -145,16 +179,22 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
145
179
|
if (!ACCEPTANCE_GATE_KEYS.has(key))
|
|
146
180
|
errors.push(`${criterionPath}.${key} is not supported.`);
|
|
147
181
|
}
|
|
148
|
-
if (typeof gate.id !== "string" || !gate.id.trim())
|
|
182
|
+
if (typeof gate.id !== "string" || !gate.id.trim()) {
|
|
149
183
|
errors.push(`${criterionPath}.id is required.`);
|
|
184
|
+
} else {
|
|
185
|
+
const normalizedId = normalizedToken(gate.id);
|
|
186
|
+
if (criterionIds.has(normalizedId))
|
|
187
|
+
errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalizedId}'.`);
|
|
188
|
+
criterionIds.add(normalizedId);
|
|
189
|
+
}
|
|
150
190
|
if (typeof gate.must !== "string" || !gate.must.trim())
|
|
151
191
|
errors.push(`${criterionPath}.must is required.`);
|
|
152
192
|
if (gate.evidence !== undefined && !Array.isArray(gate.evidence))
|
|
153
|
-
errors.push(`${criterionPath}.evidence must be an array
|
|
193
|
+
errors.push(`${criterionPath}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
|
|
154
194
|
if (Array.isArray(gate.evidence)) {
|
|
155
195
|
for (const [evidenceIndex, item] of gate.evidence.entries()) {
|
|
156
196
|
if (typeof item !== "string" || !VALID_EVIDENCE.has(item)) {
|
|
157
|
-
errors.push(`${criterionPath}.evidence[${evidenceIndex}]
|
|
197
|
+
errors.push(unsupportedEvidenceKindMessage(`${criterionPath}.evidence[${evidenceIndex}]`, item));
|
|
158
198
|
}
|
|
159
199
|
}
|
|
160
200
|
}
|
|
@@ -166,14 +206,17 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
166
206
|
if (Array.isArray(value.evidence)) {
|
|
167
207
|
for (const [index, item] of value.evidence.entries()) {
|
|
168
208
|
if (typeof item !== "string" || !VALID_EVIDENCE.has(item)) {
|
|
169
|
-
errors.push(`${pathLabel}.evidence[${index}]
|
|
209
|
+
errors.push(unsupportedEvidenceKindMessage(`${pathLabel}.evidence[${index}]`, item));
|
|
170
210
|
}
|
|
171
211
|
}
|
|
172
212
|
} else if (value.evidence !== undefined) {
|
|
173
|
-
errors.push(`${pathLabel}.evidence must be an array
|
|
213
|
+
errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
|
|
174
214
|
}
|
|
175
|
-
if (value.
|
|
215
|
+
if (value.level === "verified" && (!Array.isArray(value.verify) || value.verify.length === 0)) {
|
|
216
|
+
errors.push(`${pathLabel}.verify must contain at least one runtime command when level is verified. Use level "checked" or provide a non-empty acceptance.verify array.`);
|
|
217
|
+
} else if (value.verify !== undefined && !Array.isArray(value.verify)) {
|
|
176
218
|
errors.push(`${pathLabel}.verify must be an array.`);
|
|
219
|
+
}
|
|
177
220
|
if (Array.isArray(value.verify)) {
|
|
178
221
|
for (const [index, command] of value.verify.entries()) {
|
|
179
222
|
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
@@ -236,6 +279,23 @@ export function validateAcceptanceInput(input, pathLabel = "acceptance") {
|
|
|
236
279
|
}
|
|
237
280
|
return errors;
|
|
238
281
|
}
|
|
282
|
+
export function validateExecutionAcceptance(input) {
|
|
283
|
+
const errors = validateAcceptanceInput(input.acceptance, "acceptance");
|
|
284
|
+
for (const [index, task] of (input.tasks ?? []).entries()) {
|
|
285
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
286
|
+
}
|
|
287
|
+
for (const [stepIndex, step] of (input.chain ?? []).entries()) {
|
|
288
|
+
errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
|
|
289
|
+
if (Array.isArray(step.parallel)) {
|
|
290
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
291
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
292
|
+
}
|
|
293
|
+
} else if (step.parallel) {
|
|
294
|
+
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return errors;
|
|
298
|
+
}
|
|
239
299
|
function normalizeCriteria(criteria, evidence) {
|
|
240
300
|
return (criteria ?? []).map((criterion, index) => {
|
|
241
301
|
if (typeof criterion === "string") {
|
|
@@ -251,15 +311,28 @@ function normalizeCriteria(criteria, evidence) {
|
|
|
251
311
|
}
|
|
252
312
|
export function resolveEffectiveAcceptance(input) {
|
|
253
313
|
const explicit = normalizeAcceptanceInput(input.explicit);
|
|
254
|
-
const inferred = inferLevel(input);
|
|
255
314
|
const explicitLevel = normalizeLevel(explicit.level);
|
|
315
|
+
if (isAgentContractV1(input.agentContract)) {
|
|
316
|
+
const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto" ? "none" : explicitLevel;
|
|
317
|
+
const evidence = unique(explicit.evidence ?? []);
|
|
318
|
+
const criteria = normalizeCriteria(explicit.criteria, evidence);
|
|
319
|
+
return {
|
|
320
|
+
level,
|
|
321
|
+
explicit: input.explicit !== undefined,
|
|
322
|
+
inferredReason: [],
|
|
323
|
+
criteria,
|
|
324
|
+
evidence,
|
|
325
|
+
verify: explicit.verify ?? [],
|
|
326
|
+
review: explicit.review,
|
|
327
|
+
stopRules: explicit.stopRules ?? [],
|
|
328
|
+
reason: explicit.reason
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const inferred = inferLevel(input);
|
|
256
332
|
const level = explicitAcceptanceCanDisable(explicit) ? "none" : explicitLevel === "auto" ? inferred.level : LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferred.level] ? explicitLevel : inferred.level;
|
|
257
333
|
const evidence = unique([...level === inferred.level ? inferred.evidence : requiredEvidenceForLevel(level), ...explicit.evidence ?? []]);
|
|
258
334
|
const criteria = normalizeCriteria(explicit.criteria?.length ? explicit.criteria : inferred.criteria, evidence);
|
|
259
|
-
|
|
260
|
-
if (level === "reviewed" && explicitLevel !== "auto" && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
|
|
261
|
-
review = { ...review, required: false };
|
|
262
|
-
}
|
|
335
|
+
const review = explicit.review !== undefined ? explicit.review : inferred.review;
|
|
263
336
|
return {
|
|
264
337
|
level,
|
|
265
338
|
explicit: input.explicit !== undefined,
|
|
@@ -272,9 +345,14 @@ export function resolveEffectiveAcceptance(input) {
|
|
|
272
345
|
reason: explicit.reason
|
|
273
346
|
};
|
|
274
347
|
}
|
|
275
|
-
|
|
348
|
+
function acceptanceRequiresChildReport(acceptance) {
|
|
349
|
+
return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
|
|
350
|
+
}
|
|
351
|
+
export function formatAcceptancePrompt(acceptance, options = {}) {
|
|
276
352
|
if (acceptance.level === "none")
|
|
277
353
|
return "";
|
|
354
|
+
if (options.reportOptional && !acceptanceRequiresChildReport(acceptance))
|
|
355
|
+
return "";
|
|
278
356
|
const lines = [
|
|
279
357
|
"",
|
|
280
358
|
"## Acceptance Contract",
|
|
@@ -291,7 +369,7 @@ export function formatAcceptancePrompt(acceptance) {
|
|
|
291
369
|
for (const command of acceptance.verify)
|
|
292
370
|
lines.push(`- ${command.id}: ${command.command}`);
|
|
293
371
|
}
|
|
294
|
-
if (acceptance.review
|
|
372
|
+
if (acceptance.review) {
|
|
295
373
|
lines.push("", `Review gate: ${acceptance.review.required === false ? "optional" : "required"}${acceptance.review.agent ? ` by ${acceptance.review.agent}` : ""}.`);
|
|
296
374
|
if (acceptance.review.focus)
|
|
297
375
|
lines.push(`Review focus: ${acceptance.review.focus}`);
|
|
@@ -299,8 +377,8 @@ export function formatAcceptancePrompt(acceptance) {
|
|
|
299
377
|
if (acceptance.stopRules.length > 0) {
|
|
300
378
|
lines.push("", "Stop rules:", ...acceptance.stopRules.map((rule) => `- ${rule}`));
|
|
301
379
|
}
|
|
302
|
-
lines.push("", "Finish with a fenced JSON block tagged `acceptance-report` in this shape:", "Use empty arrays when no items apply; array fields contain strings unless object entries are shown.", "```acceptance-report", JSON.stringify({
|
|
303
|
-
criteriaSatisfied:
|
|
380
|
+
lines.push("", options.structuredOutput ? "Include an `acceptanceReport` object in your final `structured_output` tool call in this shape:" : "Finish with a fenced JSON block tagged `acceptance-report` in this shape:", "Use empty arrays when no items apply; array fields contain strings unless object entries are shown.", 'Empty-string entries (`[""]`) are ignored; use `[]` when nothing applies.', "`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.", "`commandsRun[].result` must be exactly one of: passed, failed, not-run.", "`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.", ...options.structuredOutput ? [] : ["```acceptance-report"], JSON.stringify({
|
|
381
|
+
criteriaSatisfied: acceptance.criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => ({ id: criterion.id, status: "satisfied", evidence: "specific proof" })),
|
|
304
382
|
changedFiles: ["src/file.ts"],
|
|
305
383
|
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
306
384
|
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
@@ -310,7 +388,7 @@ export function formatAcceptancePrompt(acceptance) {
|
|
|
310
388
|
diffSummary: "short description of the diff",
|
|
311
389
|
reviewFindings: ["blocker: file.ts:12 - issue found, or no blockers"],
|
|
312
390
|
manualNotes: "anything else the parent should know"
|
|
313
|
-
}, null, 2), "```");
|
|
391
|
+
}, null, 2), ...options.structuredOutput ? [] : ["```"]);
|
|
314
392
|
return lines.join(`
|
|
315
393
|
`);
|
|
316
394
|
}
|
|
@@ -343,29 +421,164 @@ function extractBalancedJson(text, start) {
|
|
|
343
421
|
}
|
|
344
422
|
return;
|
|
345
423
|
}
|
|
346
|
-
|
|
347
|
-
|
|
424
|
+
const ACCEPTANCE_REPORT_WRAPPERS = new Set(["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]);
|
|
425
|
+
const ACCEPTANCE_REPORT_FIELDS = {
|
|
426
|
+
criteriaSatisfied: "criteriaSatisfied",
|
|
427
|
+
criteria_satisfied: "criteriaSatisfied",
|
|
428
|
+
changedFiles: "changedFiles",
|
|
429
|
+
changed_files: "changedFiles",
|
|
430
|
+
testsAddedOrUpdated: "testsAddedOrUpdated",
|
|
431
|
+
tests_added_or_updated: "testsAddedOrUpdated",
|
|
432
|
+
commandsRun: "commandsRun",
|
|
433
|
+
commands_run: "commandsRun",
|
|
434
|
+
validationOutput: "validationOutput",
|
|
435
|
+
validation_output: "validationOutput",
|
|
436
|
+
residualRisks: "residualRisks",
|
|
437
|
+
residual_risks: "residualRisks",
|
|
438
|
+
noStagedFiles: "noStagedFiles",
|
|
439
|
+
no_staged_files: "noStagedFiles",
|
|
440
|
+
diffSummary: "diffSummary",
|
|
441
|
+
diff_summary: "diffSummary",
|
|
442
|
+
reviewFindings: "reviewFindings",
|
|
443
|
+
review_findings: "reviewFindings",
|
|
444
|
+
manualNotes: "manualNotes",
|
|
445
|
+
manual_notes: "manualNotes",
|
|
446
|
+
notes: "notes"
|
|
447
|
+
};
|
|
448
|
+
const CRITERION_REPORT_FIELDS = new Set(["id", "status", "evidence"]);
|
|
449
|
+
const COMMAND_REPORT_FIELDS = new Set(["command", "result", "summary"]);
|
|
450
|
+
function normalizedToken(value) {
|
|
451
|
+
return value.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
|
|
452
|
+
}
|
|
453
|
+
function normalizeCriterionStatus(value) {
|
|
454
|
+
if (typeof value !== "string")
|
|
348
455
|
return value;
|
|
349
|
-
const
|
|
350
|
-
if ("
|
|
351
|
-
return
|
|
352
|
-
if ("
|
|
353
|
-
return
|
|
456
|
+
const token = normalizedToken(value);
|
|
457
|
+
if (["satisfied", "met", "complete", "completed", "done", "pass", "passed", "success", "succeeded"].includes(token))
|
|
458
|
+
return "satisfied";
|
|
459
|
+
if (["not-satisfied", "not-met", "unmet", "incomplete", "fail", "failed"].includes(token))
|
|
460
|
+
return "not-satisfied";
|
|
461
|
+
if (["not-applicable", "n-a", "na", "skip", "skipped"].includes(token))
|
|
462
|
+
return "not-applicable";
|
|
354
463
|
return value;
|
|
355
464
|
}
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return
|
|
362
|
-
|
|
465
|
+
function normalizeCommandResult(value) {
|
|
466
|
+
if (typeof value !== "string")
|
|
467
|
+
return value;
|
|
468
|
+
const token = normalizedToken(value);
|
|
469
|
+
if (["passed", "pass", "success", "successful", "succeeded", "ok"].includes(token))
|
|
470
|
+
return "passed";
|
|
471
|
+
if (["failed", "fail", "failure", "error"].includes(token))
|
|
472
|
+
return "failed";
|
|
473
|
+
if (["not-run", "not-executed", "skip", "skipped"].includes(token))
|
|
474
|
+
return "not-run";
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
function normalizeCriterionReport(value, pathLabel, errors) {
|
|
478
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
479
|
+
return value;
|
|
480
|
+
const normalized = {};
|
|
481
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
482
|
+
if (!CRITERION_REPORT_FIELDS.has(key)) {
|
|
483
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance criterion field`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
normalized[key] = key === "id" && typeof fieldValue === "string" ? normalizedToken(fieldValue) : key === "status" ? normalizeCriterionStatus(fieldValue) : fieldValue;
|
|
487
|
+
}
|
|
488
|
+
return normalized;
|
|
489
|
+
}
|
|
490
|
+
function normalizeCommandReport(value, pathLabel, errors) {
|
|
491
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
492
|
+
return value;
|
|
493
|
+
const normalized = {};
|
|
494
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
495
|
+
if (!COMMAND_REPORT_FIELDS.has(key)) {
|
|
496
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance command field`);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
normalized[key] = key === "result" ? normalizeCommandResult(fieldValue) : fieldValue;
|
|
500
|
+
}
|
|
501
|
+
return normalized;
|
|
502
|
+
}
|
|
503
|
+
function normalizeAcceptanceReportValue(value, pathLabel = "") {
|
|
504
|
+
const errors = [];
|
|
505
|
+
let reportValue = value;
|
|
506
|
+
let reportPath = pathLabel;
|
|
507
|
+
if (reportValue && typeof reportValue === "object" && !Array.isArray(reportValue)) {
|
|
508
|
+
const record = reportValue;
|
|
509
|
+
const wrapperKeys = Object.keys(record).filter((key) => ACCEPTANCE_REPORT_WRAPPERS.has(key));
|
|
510
|
+
if (wrapperKeys.length > 0) {
|
|
511
|
+
const wrapperKey = wrapperKeys[0];
|
|
512
|
+
if (wrapperKeys.length > 1)
|
|
513
|
+
errors.push(`${pathLabel || "acceptance-report"}: multiple acceptance report wrappers are ambiguous`);
|
|
514
|
+
for (const key of Object.keys(record)) {
|
|
515
|
+
if (key !== wrapperKey)
|
|
516
|
+
errors.push(`${pathFor(pathLabel, key)}: unsupported alongside acceptance report wrapper '${wrapperKey}'`);
|
|
517
|
+
}
|
|
518
|
+
reportValue = record[wrapperKey];
|
|
519
|
+
reportPath = pathFor(pathLabel, wrapperKey);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (!reportValue || typeof reportValue !== "object" || Array.isArray(reportValue))
|
|
523
|
+
return { value: reportValue, pathLabel: reportPath, errors };
|
|
524
|
+
const normalized = {};
|
|
525
|
+
for (const [key, fieldValue] of Object.entries(reportValue)) {
|
|
526
|
+
const canonical = ACCEPTANCE_REPORT_FIELDS[key];
|
|
527
|
+
if (!canonical) {
|
|
528
|
+
errors.push(`${pathFor(reportPath, key)}: unsupported acceptance report field`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (Object.hasOwn(normalized, canonical)) {
|
|
532
|
+
errors.push(`${pathFor(reportPath, key)}: duplicates normalized field '${canonical}'`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const fieldPath = pathFor(reportPath, canonical);
|
|
536
|
+
switch (canonical) {
|
|
537
|
+
case "criteriaSatisfied": {
|
|
538
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
539
|
+
normalized[canonical] = Array.isArray(items) ? items.map((item, index) => normalizeCriterionReport(item, `${fieldPath}[${index}]`, errors)) : items;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case "commandsRun": {
|
|
543
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
544
|
+
normalized[canonical] = Array.isArray(items) ? items.map((item, index) => normalizeCommandReport(item, `${fieldPath}[${index}]`, errors)) : items;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "changedFiles":
|
|
548
|
+
case "testsAddedOrUpdated":
|
|
549
|
+
case "validationOutput":
|
|
550
|
+
case "residualRisks":
|
|
551
|
+
case "reviewFindings": {
|
|
552
|
+
const items = typeof fieldValue === "string" ? [fieldValue] : fieldValue;
|
|
553
|
+
normalized[canonical] = Array.isArray(items) ? items.filter((item) => typeof item !== "string" || item.trim().length > 0) : items;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case "noStagedFiles": {
|
|
557
|
+
const token = typeof fieldValue === "string" ? fieldValue.trim().toLowerCase() : undefined;
|
|
558
|
+
normalized[canonical] = token === "true" ? true : token === "false" ? false : fieldValue;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
default:
|
|
562
|
+
normalized[canonical] = fieldValue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return { value: normalized, pathLabel: reportPath, errors };
|
|
363
566
|
}
|
|
364
567
|
function hasGenericAcceptanceReportSignal(value) {
|
|
365
568
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
366
569
|
return false;
|
|
367
570
|
const record = value;
|
|
368
|
-
return "criteriaSatisfied" in record &&
|
|
571
|
+
return "criteriaSatisfied" in record && [
|
|
572
|
+
"changedFiles",
|
|
573
|
+
"testsAddedOrUpdated",
|
|
574
|
+
"commandsRun",
|
|
575
|
+
"validationOutput",
|
|
576
|
+
"residualRisks",
|
|
577
|
+
"noStagedFiles",
|
|
578
|
+
"diffSummary",
|
|
579
|
+
"reviewFindings",
|
|
580
|
+
"manualNotes"
|
|
581
|
+
].some((key) => (key in record));
|
|
369
582
|
}
|
|
370
583
|
function parseReportJson(body) {
|
|
371
584
|
const trimmed = body.trim();
|
|
@@ -384,31 +597,36 @@ function parseReportJson(body) {
|
|
|
384
597
|
function fencedBlocks(output, tag) {
|
|
385
598
|
return [...output.matchAll(new RegExp(`\`\`\`${tag}\\s*\\n([\\s\\S]*?)\`\`\``, "gi"))].map((match) => match[1]?.trim()).filter((value) => Boolean(value));
|
|
386
599
|
}
|
|
387
|
-
function validationPathLabelForWrapper(value) {
|
|
388
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
389
|
-
return "";
|
|
390
|
-
const record = value;
|
|
391
|
-
if ("acceptance" in record)
|
|
392
|
-
return "acceptance";
|
|
393
|
-
if ("acceptance-report" in record)
|
|
394
|
-
return "acceptance-report";
|
|
395
|
-
return "";
|
|
396
|
-
}
|
|
397
600
|
function parseAcceptanceReportBody(body) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
601
|
+
return validateAcceptanceReport(parseReportJson(body));
|
|
602
|
+
}
|
|
603
|
+
function parseUnterminatedAcceptanceReportFence(output) {
|
|
604
|
+
const opener = /```acceptance[-_]report\b[^\n]*\n/gi.exec(output);
|
|
605
|
+
if (!opener)
|
|
606
|
+
return {};
|
|
607
|
+
const bodyStart = opener.index + opener[0].length;
|
|
608
|
+
if (output.indexOf("```", bodyStart) !== -1)
|
|
609
|
+
return {};
|
|
610
|
+
try {
|
|
611
|
+
const validation = validateAcceptanceReport(JSON.parse(output.slice(bodyStart).trim()));
|
|
612
|
+
return validation.report ? { report: validation.report } : { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return { error: `Failed to parse acceptance-report: ${error instanceof Error ? error.message : String(error)}`, malformed: true };
|
|
615
|
+
}
|
|
401
616
|
}
|
|
402
617
|
function parseGenericJsonAcceptanceReportBody(body) {
|
|
403
618
|
const parsed = parseReportJson(body);
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
return;
|
|
408
|
-
|
|
619
|
+
const normalized = normalizeAcceptanceReportValue(parsed);
|
|
620
|
+
const hasCriteriaMarker = normalized.value !== null && typeof normalized.value === "object" && !Array.isArray(normalized.value) && "criteriaSatisfied" in normalized.value;
|
|
621
|
+
if (!hasGenericAcceptanceReportSignal(normalized.value) && !(hasCriteriaMarker && normalized.errors.length > 0))
|
|
622
|
+
return {};
|
|
623
|
+
const validation = validateAcceptanceReport(parsed);
|
|
624
|
+
return validation.report ? { report: validation.report } : { error: `Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
409
625
|
}
|
|
626
|
+
export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
|
|
410
627
|
export function parseAcceptanceReport(output) {
|
|
411
|
-
const
|
|
628
|
+
const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
|
|
629
|
+
const fenced = fencedBlocks(output, "acceptance[-_]report");
|
|
412
630
|
const parseErrors = [];
|
|
413
631
|
for (const body of fenced) {
|
|
414
632
|
try {
|
|
@@ -421,37 +639,65 @@ export function parseAcceptanceReport(output) {
|
|
|
421
639
|
}
|
|
422
640
|
}
|
|
423
641
|
if (parseErrors.length > 0)
|
|
424
|
-
return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}
|
|
642
|
+
return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}`, malformed: true };
|
|
643
|
+
if (explicitFencePresent) {
|
|
644
|
+
const recovered = parseUnterminatedAcceptanceReportFence(output);
|
|
645
|
+
if (recovered.report || recovered.error)
|
|
646
|
+
return recovered;
|
|
647
|
+
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence.", malformed: true };
|
|
648
|
+
}
|
|
425
649
|
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
426
650
|
try {
|
|
427
|
-
const
|
|
428
|
-
if (report)
|
|
429
|
-
return { report };
|
|
651
|
+
const parsed = parseGenericJsonAcceptanceReportBody(body);
|
|
652
|
+
if (parsed.report)
|
|
653
|
+
return { report: parsed.report };
|
|
654
|
+
if (parsed.error)
|
|
655
|
+
return { error: `Failed to parse acceptance-report: ${parsed.error}`, malformed: parsed.malformed };
|
|
430
656
|
} catch {}
|
|
431
657
|
}
|
|
432
658
|
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
433
659
|
if (markerIndex !== -1) {
|
|
434
660
|
const jsonStart = output.indexOf("{", markerIndex);
|
|
435
|
-
if (jsonStart
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
661
|
+
if (jsonStart === -1) {
|
|
662
|
+
return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:.", malformed: true };
|
|
663
|
+
}
|
|
664
|
+
const json = extractBalancedJson(output, jsonStart);
|
|
665
|
+
if (!json) {
|
|
666
|
+
return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:.", malformed: true };
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const parsed = JSON.parse(json);
|
|
670
|
+
const validation = validateAcceptanceReport(parsed);
|
|
671
|
+
if (validation.report)
|
|
672
|
+
return { report: validation.report };
|
|
673
|
+
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
676
|
+
return { error: `Failed to parse acceptance-report: ${message}`, malformed: true };
|
|
449
677
|
}
|
|
450
678
|
}
|
|
451
|
-
return { error:
|
|
679
|
+
return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
680
|
+
}
|
|
681
|
+
function parseAcceptanceReportSources(output, fileOutput) {
|
|
682
|
+
const fromText = () => parseAcceptanceReport(output);
|
|
683
|
+
const fromFile = () => {
|
|
684
|
+
if (!fileOutput)
|
|
685
|
+
return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
686
|
+
const parsed = parseAcceptanceReport(fileOutput.content);
|
|
687
|
+
return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND ? parsed : {
|
|
688
|
+
...parsed,
|
|
689
|
+
error: `${parsed.error} (in configured output ${fileOutput.path})`,
|
|
690
|
+
sourcePath: fileOutput.path
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
|
|
694
|
+
const first = primary();
|
|
695
|
+
if (first.report || first.error !== ACCEPTANCE_REPORT_NOT_FOUND)
|
|
696
|
+
return first;
|
|
697
|
+
return secondary();
|
|
452
698
|
}
|
|
453
699
|
export function stripAcceptanceReport(output) {
|
|
454
|
-
const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
700
|
+
const trailingFencePattern = /\n?```(acceptance[-_]report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
455
701
|
let trailingFence;
|
|
456
702
|
for (const match of output.matchAll(trailingFencePattern)) {
|
|
457
703
|
const end = (match.index ?? 0) + match[0].length;
|
|
@@ -460,14 +706,14 @@ export function stripAcceptanceReport(output) {
|
|
|
460
706
|
}
|
|
461
707
|
}
|
|
462
708
|
if (trailingFence) {
|
|
463
|
-
if (trailingFence.tag === "acceptance-report")
|
|
709
|
+
if (trailingFence.tag === "acceptance-report" || trailingFence.tag === "acceptance_report")
|
|
464
710
|
return output.slice(0, trailingFence.index).trimEnd();
|
|
465
711
|
try {
|
|
466
|
-
if (parseGenericJsonAcceptanceReportBody(trailingFence.body))
|
|
712
|
+
if (parseGenericJsonAcceptanceReportBody(trailingFence.body).report)
|
|
467
713
|
return output.slice(0, trailingFence.index).trimEnd();
|
|
468
714
|
} catch {}
|
|
469
715
|
}
|
|
470
|
-
return output.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "").replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "").trimEnd();
|
|
716
|
+
return output.replace(/\n?```acceptance[-_]report\s*\n[\s\S]*?```\s*$/i, "").replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "").trimEnd();
|
|
471
717
|
}
|
|
472
718
|
function isStringArray(value) {
|
|
473
719
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
@@ -499,12 +745,15 @@ function validateStringArrayField(errors, value, pathLabel) {
|
|
|
499
745
|
return;
|
|
500
746
|
}
|
|
501
747
|
for (const [index, item] of value.entries()) {
|
|
502
|
-
if (typeof item !== "string")
|
|
503
|
-
pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
|
|
748
|
+
if (typeof item !== "string" || !item.trim())
|
|
749
|
+
pushTypeError(errors, `${pathLabel}[${index}]`, "non-empty string", item);
|
|
504
750
|
}
|
|
505
751
|
}
|
|
506
752
|
function validateAcceptanceReport(value, pathLabel = "") {
|
|
507
|
-
const
|
|
753
|
+
const normalized = normalizeAcceptanceReportValue(value, pathLabel);
|
|
754
|
+
value = normalized.value;
|
|
755
|
+
pathLabel = normalized.pathLabel;
|
|
756
|
+
const errors = normalized.errors;
|
|
508
757
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
509
758
|
pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
|
|
510
759
|
return { errors };
|
|
@@ -514,6 +763,7 @@ function validateAcceptanceReport(value, pathLabel = "") {
|
|
|
514
763
|
if (!Array.isArray(report.criteriaSatisfied)) {
|
|
515
764
|
pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
|
|
516
765
|
} else {
|
|
766
|
+
const criterionIds = new Set;
|
|
517
767
|
for (const [index, item] of report.criteriaSatisfied.entries()) {
|
|
518
768
|
const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
|
|
519
769
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -521,8 +771,13 @@ function validateAcceptanceReport(value, pathLabel = "") {
|
|
|
521
771
|
continue;
|
|
522
772
|
}
|
|
523
773
|
const criterion = item;
|
|
524
|
-
if (criterion.id !== undefined && typeof criterion.id !== "string")
|
|
774
|
+
if (criterion.id !== undefined && typeof criterion.id !== "string") {
|
|
525
775
|
pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
|
|
776
|
+
} else if (typeof criterion.id === "string" && criterion.id) {
|
|
777
|
+
if (criterionIds.has(criterion.id))
|
|
778
|
+
errors.push(`${itemPath}.id: duplicate normalized criterion id '${criterion.id}'`);
|
|
779
|
+
criterionIds.add(criterion.id);
|
|
780
|
+
}
|
|
526
781
|
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
|
|
527
782
|
pushTypeError(errors, `${itemPath}.status`, 'one of "satisfied", "not-satisfied", "not-applicable"', criterion.status);
|
|
528
783
|
}
|
|
@@ -551,8 +806,8 @@ function validateAcceptanceReport(value, pathLabel = "") {
|
|
|
551
806
|
if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
|
|
552
807
|
pushTypeError(errors, `${itemPath}.result`, 'one of "passed", "failed", "not-run"', command.result);
|
|
553
808
|
}
|
|
554
|
-
if (typeof command.summary !== "string")
|
|
555
|
-
pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
|
|
809
|
+
if (typeof command.summary !== "string" || !command.summary.trim())
|
|
810
|
+
pushTypeError(errors, `${itemPath}.summary`, "non-empty string", command.summary);
|
|
556
811
|
}
|
|
557
812
|
}
|
|
558
813
|
}
|
|
@@ -562,8 +817,8 @@ function validateAcceptanceReport(value, pathLabel = "") {
|
|
|
562
817
|
validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
|
|
563
818
|
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean")
|
|
564
819
|
pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
|
|
565
|
-
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string")
|
|
566
|
-
pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
|
|
820
|
+
if (report.diffSummary !== undefined && (typeof report.diffSummary !== "string" || !report.diffSummary.trim()))
|
|
821
|
+
pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "non-empty string", report.diffSummary);
|
|
567
822
|
if (report.reviewFindings !== undefined)
|
|
568
823
|
validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
|
|
569
824
|
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string")
|
|
@@ -576,9 +831,9 @@ function validateAcceptanceReport(value, pathLabel = "") {
|
|
|
576
831
|
return hasReportField ? { report, errors } : { errors: [`${pathLabel || "acceptance-report"}: expected at least one acceptance report field`] };
|
|
577
832
|
}
|
|
578
833
|
function checkCriteriaSatisfied(criteria, report) {
|
|
579
|
-
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id, item]));
|
|
834
|
+
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [normalizedToken(item.id), item]));
|
|
580
835
|
return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
|
|
581
|
-
const item = reports.get(criterion.id);
|
|
836
|
+
const item = reports.get(normalizedToken(criterion.id));
|
|
582
837
|
if (!item)
|
|
583
838
|
return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
|
|
584
839
|
if (item.status !== "satisfied")
|
|
@@ -586,30 +841,34 @@ function checkCriteriaSatisfied(criteria, report) {
|
|
|
586
841
|
return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
|
|
587
842
|
});
|
|
588
843
|
}
|
|
589
|
-
function
|
|
844
|
+
function reportEvidenceStatus(report, kind) {
|
|
590
845
|
switch (kind) {
|
|
591
846
|
case "changed-files":
|
|
592
|
-
|
|
847
|
+
if (!isStringArray(report.changedFiles))
|
|
848
|
+
return "failed";
|
|
849
|
+
return report.changedFiles.length === 0 ? "not-applicable" : "passed";
|
|
593
850
|
case "tests-added":
|
|
594
|
-
|
|
851
|
+
if (!isStringArray(report.testsAddedOrUpdated))
|
|
852
|
+
return "failed";
|
|
853
|
+
return report.testsAddedOrUpdated.length === 0 ? "not-applicable" : "passed";
|
|
595
854
|
case "commands-run":
|
|
596
|
-
return Array.isArray(report.commandsRun) && report.commandsRun.length > 0;
|
|
855
|
+
return Array.isArray(report.commandsRun) && report.commandsRun.length > 0 ? "passed" : "failed";
|
|
597
856
|
case "validation-output":
|
|
598
|
-
return isStringArray(report.validationOutput) && report.validationOutput.length > 0;
|
|
857
|
+
return isStringArray(report.validationOutput) && report.validationOutput.length > 0 ? "passed" : "failed";
|
|
599
858
|
case "residual-risks":
|
|
600
|
-
return isStringArray(report.residualRisks);
|
|
859
|
+
return isStringArray(report.residualRisks) ? "passed" : "failed";
|
|
601
860
|
case "no-staged-files":
|
|
602
|
-
return report.noStagedFiles === true;
|
|
861
|
+
return report.noStagedFiles === true ? "passed" : "failed";
|
|
603
862
|
case "diff-summary":
|
|
604
|
-
return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
|
|
863
|
+
return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0 ? "passed" : "failed";
|
|
605
864
|
case "review-findings":
|
|
606
|
-
return isStringArray(report.reviewFindings);
|
|
865
|
+
return isStringArray(report.reviewFindings) ? "passed" : "failed";
|
|
607
866
|
case "manual-notes":
|
|
608
|
-
return Boolean((report.manualNotes ?? report.notes)?.trim());
|
|
867
|
+
return Boolean((report.manualNotes ?? report.notes)?.trim()) ? "passed" : "failed";
|
|
609
868
|
}
|
|
610
869
|
}
|
|
611
870
|
function checkNoStagedFiles(cwd) {
|
|
612
|
-
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
|
|
871
|
+
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8", windowsHide: true });
|
|
613
872
|
if (result.status !== 0) {
|
|
614
873
|
return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
|
|
615
874
|
}
|
|
@@ -619,11 +878,13 @@ function checkNoStagedFiles(cwd) {
|
|
|
619
878
|
function runStructuralChecks(acceptance, report, cwd) {
|
|
620
879
|
const checks = [];
|
|
621
880
|
for (const kind of acceptance.evidence) {
|
|
622
|
-
|
|
881
|
+
if (kind === "no-staged-files" && report.noStagedFiles === undefined)
|
|
882
|
+
continue;
|
|
883
|
+
const status = reportEvidenceStatus(report, kind);
|
|
623
884
|
checks.push({
|
|
624
885
|
id: `evidence:${kind}`,
|
|
625
|
-
status
|
|
626
|
-
message:
|
|
886
|
+
status,
|
|
887
|
+
message: status === "passed" ? `${kind} evidence present.` : status === "not-applicable" ? `${kind} evidence explicitly reported as not applicable.` : `${kind} evidence missing from child report.`
|
|
627
888
|
});
|
|
628
889
|
}
|
|
629
890
|
if (acceptance.evidence.includes("no-staged-files"))
|
|
@@ -637,6 +898,25 @@ function trimOutput(value) {
|
|
|
637
898
|
return trimmed.length > 12000 ? `${trimmed.slice(0, 12000)}
|
|
638
899
|
...[truncated]` : trimmed;
|
|
639
900
|
}
|
|
901
|
+
const SENSITIVE_ENV_KEY_PATTERN = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASS|AUTH|CREDENTIAL|COOKIE|SESSION|PRIVATE|API_KEY|ACCESS_KEY)(?:_|$)/i;
|
|
902
|
+
function effectiveVerifyEnv(env) {
|
|
903
|
+
const inherited = Object.fromEntries(Object.entries(process.env).flatMap(([key, value]) => {
|
|
904
|
+
return typeof value === "string" ? [[key, value]] : [];
|
|
905
|
+
}));
|
|
906
|
+
return { ...inherited, ...env ?? {} };
|
|
907
|
+
}
|
|
908
|
+
function verifyRedactionEnv(env) {
|
|
909
|
+
return Object.fromEntries(Object.entries(effectiveVerifyEnv(env)).filter(([key, value]) => {
|
|
910
|
+
return value.length >= 4 && SENSITIVE_ENV_KEY_PATTERN.test(key);
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
function redactVerifyEnv(value, env) {
|
|
914
|
+
let redacted = value;
|
|
915
|
+
const secrets = [...new Set(Object.values(verifyRedactionEnv(env)).filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
916
|
+
for (const secret of secrets)
|
|
917
|
+
redacted = redacted.replaceAll(secret, "[REDACTED]");
|
|
918
|
+
return redacted;
|
|
919
|
+
}
|
|
640
920
|
function uniqueStrings(items) {
|
|
641
921
|
return unique(items.map((item) => item?.trim()).filter((item) => Boolean(item)));
|
|
642
922
|
}
|
|
@@ -668,6 +948,119 @@ export function aggregateAcceptanceReport(input) {
|
|
|
668
948
|
notes: input.notes
|
|
669
949
|
};
|
|
670
950
|
}
|
|
951
|
+
const DEFAULT_VERIFY_TIMEOUT_MS = 120000;
|
|
952
|
+
function hash(value) {
|
|
953
|
+
return createHash("sha256").update(value).digest("hex");
|
|
954
|
+
}
|
|
955
|
+
function readVerifyWorkspaceState(cwd) {
|
|
956
|
+
const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", windowsHide: true });
|
|
957
|
+
if (repo.status !== 0 || !repo.stdout.trim())
|
|
958
|
+
return;
|
|
959
|
+
const repoRoot = fs.realpathSync(repo.stdout.trim());
|
|
960
|
+
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8", windowsHide: true });
|
|
961
|
+
const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, windowsHide: true });
|
|
962
|
+
if (head.status !== 0 || diff.status !== 0 || !head.stdout.trim())
|
|
963
|
+
return;
|
|
964
|
+
return {
|
|
965
|
+
kind: "git-tracked",
|
|
966
|
+
repoRoot,
|
|
967
|
+
cwdRelative: path.relative(repoRoot, fs.realpathSync(cwd)) || ".",
|
|
968
|
+
head: head.stdout.trim(),
|
|
969
|
+
diffHash: hash(diff.stdout)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function isCachedVerifyResult(value) {
|
|
973
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
974
|
+
return false;
|
|
975
|
+
const result = value;
|
|
976
|
+
return typeof result.id === "string" && typeof result.command === "string" && (typeof result.exitCode === "number" || result.exitCode === null) && (result.status === "passed" || result.status === "failed" || result.status === "timed-out" || result.status === "allowed-failure") && typeof result.durationMs === "number";
|
|
977
|
+
}
|
|
978
|
+
async function runMemoizedVerifyCommand(command, defaultCwd, options = {}) {
|
|
979
|
+
const cwd = command.cwd ? path.resolve(defaultCwd, command.cwd) : defaultCwd;
|
|
980
|
+
let workspaceState;
|
|
981
|
+
try {
|
|
982
|
+
workspaceState = readVerifyWorkspaceState(cwd);
|
|
983
|
+
} catch {
|
|
984
|
+
workspaceState = undefined;
|
|
985
|
+
}
|
|
986
|
+
if (!workspaceState || !options.artifactsDir || !options.runId) {
|
|
987
|
+
return runVerifyCommand(command, defaultCwd, options);
|
|
988
|
+
}
|
|
989
|
+
const envKeys = Object.keys(command.env ?? {}).sort();
|
|
990
|
+
const envHash = hash(JSON.stringify(Object.fromEntries(Object.entries(effectiveVerifyEnv(command.env)).sort(([left], [right]) => left.localeCompare(right)))));
|
|
991
|
+
const timeoutMs = command.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
992
|
+
const cacheKey = hash(JSON.stringify({
|
|
993
|
+
version: 1,
|
|
994
|
+
command: command.command,
|
|
995
|
+
cwdRelative: workspaceState.cwdRelative,
|
|
996
|
+
envKeys,
|
|
997
|
+
envHash,
|
|
998
|
+
timeoutMs,
|
|
999
|
+
allowFailure: command.allowFailure === true,
|
|
1000
|
+
head: workspaceState.head,
|
|
1001
|
+
diffHash: workspaceState.diffHash
|
|
1002
|
+
}));
|
|
1003
|
+
const artifactPath = path.join(options.artifactsDir, "acceptance", "verify", options.runId, `${cacheKey}.json`);
|
|
1004
|
+
try {
|
|
1005
|
+
const cached = JSON.parse(fs.readFileSync(artifactPath, "utf-8"));
|
|
1006
|
+
if (cached.cacheKey === cacheKey && isCachedVerifyResult(cached.result)) {
|
|
1007
|
+
return { ...cached.result, id: command.id, command: command.command, cwd, artifactPath, cacheKey, memoized: true, envKeys, envHash, workspaceState };
|
|
1008
|
+
}
|
|
1009
|
+
} catch {}
|
|
1010
|
+
const result = await runVerifyCommand(command, defaultCwd, options);
|
|
1011
|
+
const evidenced = { ...result, artifactPath, cacheKey, memoized: false, envKeys, envHash, workspaceState };
|
|
1012
|
+
try {
|
|
1013
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
1014
|
+
fs.writeFileSync(artifactPath, JSON.stringify({
|
|
1015
|
+
version: 1,
|
|
1016
|
+
cacheKey,
|
|
1017
|
+
command: command.command,
|
|
1018
|
+
cwdRelative: workspaceState.cwdRelative,
|
|
1019
|
+
envKeys,
|
|
1020
|
+
envHash,
|
|
1021
|
+
timeoutMs,
|
|
1022
|
+
allowFailure: command.allowFailure === true,
|
|
1023
|
+
workspaceState,
|
|
1024
|
+
result: evidenced
|
|
1025
|
+
}, null, 2), "utf-8");
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
evidenced.artifactError = error instanceof Error ? error.message : String(error);
|
|
1028
|
+
delete evidenced.artifactPath;
|
|
1029
|
+
}
|
|
1030
|
+
return evidenced;
|
|
1031
|
+
}
|
|
1032
|
+
export function quoteExecutableForShell(command, platform = process.platform) {
|
|
1033
|
+
if (platform !== "win32")
|
|
1034
|
+
return command;
|
|
1035
|
+
const trimmed = command.trimStart();
|
|
1036
|
+
if (trimmed.startsWith('"'))
|
|
1037
|
+
return command;
|
|
1038
|
+
const firstToken = trimmed.match(/^\S+/)?.[0];
|
|
1039
|
+
if (/\.(?:exe|bat|cmd|com|ps1)$/i.test(firstToken ?? ""))
|
|
1040
|
+
return command;
|
|
1041
|
+
const match = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?\\[^"<>|&*?\r\n]*?\.(?:exe|bat|cmd|com|ps1))(?=\s|$)/i);
|
|
1042
|
+
const executable = match?.[1];
|
|
1043
|
+
if (executable && /\s/.test(executable) && !/[A-Za-z]:\\/.test(executable.slice(3))) {
|
|
1044
|
+
const rest = trimmed.slice(executable.length);
|
|
1045
|
+
const leading = command.slice(0, command.length - trimmed.length);
|
|
1046
|
+
return `${leading}"${executable}"${rest}`;
|
|
1047
|
+
}
|
|
1048
|
+
const extensionlessSpacedFilenameMatch = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?)(?=\s+--|$)/i);
|
|
1049
|
+
const extensionlessSpacedFilename = extensionlessSpacedFilenameMatch?.[1];
|
|
1050
|
+
if (extensionlessSpacedFilename && /\s/.test(extensionlessSpacedFilename.slice(0, extensionlessSpacedFilename.lastIndexOf("\\"))) && !/[A-Za-z]:\\/.test(extensionlessSpacedFilename.slice(3))) {
|
|
1051
|
+
const rest = trimmed.slice(extensionlessSpacedFilename.length);
|
|
1052
|
+
const leading = command.slice(0, command.length - trimmed.length);
|
|
1053
|
+
return `${leading}"${extensionlessSpacedFilename}"${rest}`;
|
|
1054
|
+
}
|
|
1055
|
+
const extensionlessMatch = trimmed.match(/^([A-Za-z]:\\[^"<>|&*?\r\n]*?\s[^"<>|&*?\r\n]*?\\[^"<>|&*?\s\r\n]+)(?=\s|$)/i);
|
|
1056
|
+
const extensionlessExecutable = extensionlessMatch?.[1];
|
|
1057
|
+
if (extensionlessExecutable && /\s/.test(extensionlessExecutable) && !/[A-Za-z]:\\/.test(extensionlessExecutable.slice(3)) && !/\s/.test(extensionlessExecutable.slice(extensionlessExecutable.lastIndexOf("\\") + 1))) {
|
|
1058
|
+
const rest = trimmed.slice(extensionlessExecutable.length);
|
|
1059
|
+
const leading = command.slice(0, command.length - trimmed.length);
|
|
1060
|
+
return `${leading}"${extensionlessExecutable}"${rest}`;
|
|
1061
|
+
}
|
|
1062
|
+
return command;
|
|
1063
|
+
}
|
|
671
1064
|
function runVerifyCommand(command, defaultCwd, options = {}) {
|
|
672
1065
|
return new Promise((resolve) => {
|
|
673
1066
|
const startedAt = Date.now();
|
|
@@ -677,9 +1070,9 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
|
|
|
677
1070
|
let timedOut = false;
|
|
678
1071
|
let settled = false;
|
|
679
1072
|
let hardKill;
|
|
680
|
-
const child = spawn(command.command, {
|
|
1073
|
+
const child = spawn(quoteExecutableForShell(command.command), {
|
|
681
1074
|
cwd,
|
|
682
|
-
env:
|
|
1075
|
+
env: effectiveVerifyEnv(command.env),
|
|
683
1076
|
shell: true,
|
|
684
1077
|
stdio: ["ignore", "pipe", "pipe"],
|
|
685
1078
|
windowsHide: true
|
|
@@ -710,13 +1103,13 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
|
|
|
710
1103
|
finish({
|
|
711
1104
|
exitCode: null,
|
|
712
1105
|
status: "timed-out",
|
|
713
|
-
stdout: trimOutput(stdout),
|
|
714
|
-
stderr: trimOutput(stderr || options.abortMessage || "Acceptance verification timed out.")
|
|
1106
|
+
stdout: trimOutput(redactVerifyEnv(stdout, command.env)),
|
|
1107
|
+
stderr: trimOutput(redactVerifyEnv(stderr || options.abortMessage || "Acceptance verification timed out.", command.env))
|
|
715
1108
|
});
|
|
716
1109
|
}, 1000);
|
|
717
1110
|
hardKill.unref?.();
|
|
718
1111
|
};
|
|
719
|
-
const timeout = setTimeout(abortVerification, command.timeoutMs ??
|
|
1112
|
+
const timeout = setTimeout(abortVerification, command.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS);
|
|
720
1113
|
timeout.unref?.();
|
|
721
1114
|
if (options.signal?.aborted)
|
|
722
1115
|
abortVerification();
|
|
@@ -733,23 +1126,25 @@ function runVerifyCommand(command, defaultCwd, options = {}) {
|
|
|
733
1126
|
finish({
|
|
734
1127
|
exitCode,
|
|
735
1128
|
status: timedOut ? "timed-out" : passed ? "passed" : command.allowFailure ? "allowed-failure" : "failed",
|
|
736
|
-
stdout: trimOutput(stdout),
|
|
737
|
-
stderr: trimOutput(stderr || (timedOut ? options.abortMessage ?? "" : ""))
|
|
1129
|
+
stdout: trimOutput(redactVerifyEnv(stdout, command.env)),
|
|
1130
|
+
stderr: trimOutput(redactVerifyEnv(stderr || (timedOut ? options.abortMessage ?? "" : ""), command.env))
|
|
738
1131
|
});
|
|
739
1132
|
});
|
|
740
1133
|
child.on("error", (error) => {
|
|
741
1134
|
finish({
|
|
742
1135
|
exitCode: timedOut ? null : 1,
|
|
743
1136
|
status: timedOut ? "timed-out" : command.allowFailure ? "allowed-failure" : "failed",
|
|
744
|
-
stderr: timedOut ? trimOutput(stderr || options.abortMessage || "Acceptance verification timed out.") : error instanceof Error ? error.message : String(error)
|
|
1137
|
+
stderr: timedOut ? trimOutput(redactVerifyEnv(stderr || options.abortMessage || "Acceptance verification timed out.", command.env)) : redactVerifyEnv(error instanceof Error ? error.message : String(error), command.env)
|
|
745
1138
|
});
|
|
746
1139
|
});
|
|
747
1140
|
});
|
|
748
1141
|
}
|
|
749
1142
|
export async function evaluateAcceptance(input) {
|
|
750
1143
|
const acceptance = input.acceptance;
|
|
1144
|
+
const initialStatus = acceptance.level === "none" ? "not-required" : "claimed";
|
|
751
1145
|
const ledger = {
|
|
752
|
-
status:
|
|
1146
|
+
status: initialStatus,
|
|
1147
|
+
evidenceStatus: initialStatus,
|
|
753
1148
|
explicit: acceptance.explicit,
|
|
754
1149
|
effectiveAcceptance: acceptance,
|
|
755
1150
|
inferredReason: acceptance.inferredReason,
|
|
@@ -759,65 +1154,121 @@ export async function evaluateAcceptance(input) {
|
|
|
759
1154
|
};
|
|
760
1155
|
if (acceptance.level === "none")
|
|
761
1156
|
return ledger;
|
|
762
|
-
const parsed = input.
|
|
1157
|
+
const parsed = input.reportError ? { error: input.reportError } : input.report ? (() => {
|
|
1158
|
+
const validation = validateAcceptanceReport(input.report);
|
|
1159
|
+
return validation.report ? { report: validation.report } : { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
1160
|
+
})() : parseAcceptanceReportSources(input.output, input.fileOutput);
|
|
1161
|
+
const durableFileOutput = input.fileOutput?.authoritative && input.fileOutput.durable ? input.fileOutput : undefined;
|
|
1162
|
+
if (parsed.malformed && parsed.sourcePath && durableFileOutput) {
|
|
1163
|
+
ledger.recovery = {
|
|
1164
|
+
status: "available-for-review",
|
|
1165
|
+
reason: "acceptance-metadata-rejected",
|
|
1166
|
+
reportPath: parsed.sourcePath,
|
|
1167
|
+
reportHash: hash(durableFileOutput.content)
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const needsReport = acceptanceRequiresChildReport(acceptance);
|
|
763
1171
|
if (parsed.report) {
|
|
764
1172
|
ledger.childReport = parsed.report;
|
|
765
1173
|
ledger.status = "attested";
|
|
766
|
-
|
|
1174
|
+
ledger.evidenceStatus = "attested";
|
|
1175
|
+
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
767
1176
|
ledger.childReportParseError = parsed.error;
|
|
768
1177
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
769
|
-
ledger.
|
|
770
|
-
|
|
1178
|
+
if (ledger.recovery) {
|
|
1179
|
+
ledger.status = "rejected";
|
|
1180
|
+
ledger.evidenceStatus = "rejected";
|
|
1181
|
+
}
|
|
1182
|
+
if (!input.reportOptional) {
|
|
1183
|
+
ledger.status = "rejected";
|
|
1184
|
+
ledger.evidenceStatus = "rejected";
|
|
1185
|
+
return ledger;
|
|
1186
|
+
}
|
|
1187
|
+
} else {
|
|
1188
|
+
ledger.childReportParseError = parsed.error;
|
|
771
1189
|
}
|
|
772
|
-
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
1190
|
+
if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
773
1191
|
ledger.runtimeChecks = [
|
|
1192
|
+
...ledger.runtimeChecks,
|
|
774
1193
|
...checkCriteriaSatisfied(acceptance.criteria, parsed.report),
|
|
775
1194
|
...runStructuralChecks(acceptance, parsed.report, input.cwd)
|
|
776
1195
|
];
|
|
777
|
-
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
778
|
-
ledger.status = "
|
|
779
|
-
|
|
1196
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1197
|
+
ledger.status = "checked";
|
|
1198
|
+
ledger.evidenceStatus = "checked";
|
|
780
1199
|
}
|
|
781
|
-
ledger.status = "checked";
|
|
782
1200
|
}
|
|
783
1201
|
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || acceptance.verify.length > 0)) {
|
|
784
1202
|
if (acceptance.level === "verified" && acceptance.verify.length === 0) {
|
|
785
1203
|
ledger.runtimeChecks.push({ id: "verification-config", status: "failed", message: "verified acceptance requires runtime verify commands." });
|
|
786
1204
|
ledger.status = "rejected";
|
|
1205
|
+
ledger.evidenceStatus = "rejected";
|
|
787
1206
|
return ledger;
|
|
788
1207
|
}
|
|
789
1208
|
ledger.verifyRuns = [];
|
|
790
1209
|
for (const command of acceptance.verify) {
|
|
791
|
-
ledger.verifyRuns.push(await
|
|
1210
|
+
ledger.verifyRuns.push(await runMemoizedVerifyCommand(command, input.cwd, {
|
|
1211
|
+
signal: input.signal,
|
|
1212
|
+
abortMessage: input.abortMessage,
|
|
1213
|
+
artifactsDir: input.artifactsDir,
|
|
1214
|
+
runId: input.runId
|
|
1215
|
+
}));
|
|
792
1216
|
if (input.signal?.aborted)
|
|
793
1217
|
break;
|
|
794
1218
|
}
|
|
795
1219
|
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
796
1220
|
ledger.status = "rejected";
|
|
1221
|
+
ledger.evidenceStatus = "rejected";
|
|
797
1222
|
return ledger;
|
|
798
1223
|
}
|
|
799
|
-
ledger.status
|
|
1224
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1225
|
+
ledger.status = "verified";
|
|
1226
|
+
ledger.evidenceStatus = "verified";
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1230
|
+
ledger.status = "rejected";
|
|
1231
|
+
ledger.evidenceStatus = "rejected";
|
|
1232
|
+
return ledger;
|
|
800
1233
|
}
|
|
801
|
-
if (
|
|
802
|
-
|
|
1234
|
+
if (ledger.status === "claimed") {
|
|
1235
|
+
ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
|
|
1236
|
+
ledger.evidenceStatus = ledger.status;
|
|
1237
|
+
}
|
|
1238
|
+
if (acceptance.review) {
|
|
1239
|
+
if (input.reviewResult?.status === "reviewed") {
|
|
803
1240
|
ledger.reviewResult = input.reviewResult;
|
|
804
|
-
ledger.status =
|
|
805
|
-
} else {
|
|
806
|
-
|
|
807
|
-
ledger.
|
|
808
|
-
|
|
1241
|
+
ledger.status = "reviewed";
|
|
1242
|
+
} else if (input.reviewResult?.status === "blockers") {
|
|
1243
|
+
ledger.reviewResult = input.reviewResult;
|
|
1244
|
+
ledger.status = "rejected";
|
|
1245
|
+
} else if (acceptance.review.required !== false) {
|
|
1246
|
+
ledger.reviewResult = input.reviewResult ?? {
|
|
1247
|
+
status: "review-required",
|
|
809
1248
|
findings: [{
|
|
810
|
-
severity:
|
|
811
|
-
issue: "
|
|
1249
|
+
severity: "non-blocking",
|
|
1250
|
+
issue: "Independent review has not been supplied.",
|
|
812
1251
|
rationale: "The run cannot be marked reviewed from child evidence alone."
|
|
813
1252
|
}]
|
|
814
1253
|
};
|
|
815
|
-
|
|
816
|
-
ledger.status = "rejected";
|
|
1254
|
+
ledger.status = "review-required";
|
|
817
1255
|
}
|
|
818
1256
|
}
|
|
819
1257
|
return ledger;
|
|
820
1258
|
}
|
|
1259
|
+
export function buildSkippedAcceptanceLedger(acceptance, input) {
|
|
1260
|
+
const status = acceptance.level === "none" ? "not-required" : "rejected";
|
|
1261
|
+
return {
|
|
1262
|
+
status,
|
|
1263
|
+
evidenceStatus: status,
|
|
1264
|
+
explicit: acceptance.explicit,
|
|
1265
|
+
effectiveAcceptance: acceptance,
|
|
1266
|
+
inferredReason: acceptance.inferredReason,
|
|
1267
|
+
criteria: acceptance.criteria,
|
|
1268
|
+
runtimeChecks: acceptance.level === "none" ? [] : [{ id: input.id, status: "failed", message: input.message }],
|
|
1269
|
+
verifyRuns: []
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
821
1272
|
export function acceptanceFailureMessage(ledger) {
|
|
822
1273
|
if (ledger.status !== "rejected")
|
|
823
1274
|
return;
|
|
@@ -827,8 +1278,6 @@ export function acceptanceFailureMessage(ledger) {
|
|
|
827
1278
|
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
828
1279
|
if (failedVerify)
|
|
829
1280
|
return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
830
|
-
if (ledger.reviewResult?.status === "needs-parent-decision")
|
|
831
|
-
return "Acceptance review required but no automatic reviewer result is available.";
|
|
832
1281
|
if (ledger.reviewResult?.status === "blockers")
|
|
833
1282
|
return "Acceptance review found blockers.";
|
|
834
1283
|
return "Acceptance rejected.";
|