@kontourai/flow-agents 3.3.0 → 3.4.1
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/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +48 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +56 -3
- package/build/src/lib/flow-resolver.js +151 -11
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +101 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +153 -10
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Scope: the gate evaluates the session's current task (.kontourai/flow-agents/current.json)
|
|
19
19
|
* when set, so an unrelated active workflow elsewhere in the repo does not gate
|
|
20
|
-
* this stop.
|
|
21
|
-
*
|
|
22
|
-
* capture log or evidence.json contradicts) block before execution begins.
|
|
20
|
+
* this stop. A pre-execution sidecar remains warning-only unless it has an active
|
|
21
|
+
* canonical Flow run; once Flow is active, its unfinished gate remains blocking.
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
24
|
'use strict';
|
|
@@ -42,9 +41,12 @@ const {
|
|
|
42
41
|
const {
|
|
43
42
|
flowAgentsArtifactRoot,
|
|
44
43
|
flowAgentsArtifactRootsForRead,
|
|
44
|
+
resolveSharedRepoRoot,
|
|
45
|
+
warnIfFailingOpenInsideGitTree,
|
|
45
46
|
} = require('./lib/local-artifact-paths');
|
|
46
47
|
const { resolveActor, isUnresolvedActor, detectRuntime } = require('./lib/actor-identity.js');
|
|
47
48
|
const { readCurrentPointer } = require('./lib/current-pointer.js');
|
|
49
|
+
const { isRunnableCommandText, isAmbiguousAbsenceCommand } = require('./lib/runnable-command.js');
|
|
48
50
|
|
|
49
51
|
const MAX_STDIN = 1024 * 1024;
|
|
50
52
|
const ACTIVE_STATUSES = new Set([
|
|
@@ -66,10 +68,9 @@ const WORKFLOW_SESSION_TYPES = new Set(['deliver', 'delivery', 'fix-bug', 'execu
|
|
|
66
68
|
const SIDECAR_NAMES = new Set(['state.json', 'handoff.json', 'trust.bundle']);
|
|
67
69
|
const OPTIONAL_SIDECAR_NAMES = new Set();
|
|
68
70
|
|
|
69
|
-
// A
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// onward gates fully.
|
|
71
|
+
// A sidecar that has not started execution is normally expected to be incomplete.
|
|
72
|
+
// An active canonical Flow run is the exception: its current gate remains live and
|
|
73
|
+
// blocks Stop even while the projected sidecar is in pickup or planning.
|
|
73
74
|
const PRE_EXECUTION_STATUSES = new Set(['new', 'planning', 'planned', 'backlog']);
|
|
74
75
|
const PRE_EXECUTION_PHASES = new Set(['idea', 'backlog', 'pickup', 'planning']);
|
|
75
76
|
|
|
@@ -78,19 +79,48 @@ const PRE_EXECUTION_PHASES = new Set(['idea', 'backlog', 'pickup', 'planning']);
|
|
|
78
79
|
// not block an unrelated session.
|
|
79
80
|
const TERMINAL_STATUSES = new Set(['done', 'delivered', 'accepted', 'archived', 'complete', 'completed']);
|
|
80
81
|
|
|
82
|
+
function isTerminalDeliveredState(state) {
|
|
83
|
+
if (!state || typeof state !== 'object') return false;
|
|
84
|
+
const status = normalizedStatus(state.status || '');
|
|
85
|
+
const phase = normalizedStatus(state.phase || '');
|
|
86
|
+
return TERMINAL_STATUSES.has(status) || phase === 'done';
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
function parseJson(raw) {
|
|
82
90
|
try { return JSON.parse(raw || '{}'); } catch { return {}; }
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function findRepoRoot(startDir) {
|
|
86
|
-
|
|
94
|
+
const resolvedStartDir = path.resolve(startDir || process.cwd());
|
|
95
|
+
// #357: prefer git's SHARED common-dir root (the primary checkout's repo root, resolved
|
|
96
|
+
// identically from any linked worktree) before falling back to the plain `.git`/AGENTS.md
|
|
97
|
+
// ancestor walk below. Without this, a worktree's OWN `.git` FILE (not the shared `.git`
|
|
98
|
+
// DIRECTORY) satisfies `fs.existsSync(path.join(dir, '.git'))` at the very first directory
|
|
99
|
+
// checked, so the walk always stopped at the worktree root instead of resolving to the
|
|
100
|
+
// shared primary checkout — see src/lib/local-artifact-root.ts's resolveSharedRepoRoot doc
|
|
101
|
+
// comment for the full rationale. Fails open to the walk below (unchanged) when git is
|
|
102
|
+
// unavailable, `resolvedStartDir` is not inside a git working tree, or the command fails —
|
|
103
|
+
// this is also exactly today's behavior in the single-checkout case, where
|
|
104
|
+
// `--git-common-dir` resolves to `.git` under `resolvedStartDir` itself.
|
|
105
|
+
const sharedRoot = resolveSharedRepoRoot(resolvedStartDir);
|
|
106
|
+
if (sharedRoot) return sharedRoot;
|
|
107
|
+
let dir = resolvedStartDir;
|
|
87
108
|
const root = path.parse(dir).root;
|
|
88
109
|
for (let depth = 0; dir && depth < 40; depth++) {
|
|
89
|
-
if (fs.existsSync(path.join(dir, '.git'))
|
|
110
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
111
|
+
// #413 iteration-2 Fix 1: resolveSharedRepoRoot failed above even though a `.git` entry
|
|
112
|
+
// exists right here — git resolution was ATTEMPTED and FAILED (corrupted gitlink, bad
|
|
113
|
+
// GIT_DIR, git unavailable, etc.), not merely "no git repo at all". Loud, not silent —
|
|
114
|
+
// see warnIfFailingOpenInsideGitTree's own doc comment for the full rationale. This walk
|
|
115
|
+
// still returns `dir` (the ancestor-walk fallback is preserved unchanged) after warning.
|
|
116
|
+
warnIfFailingOpenInsideGitTree(resolvedStartDir, dir);
|
|
117
|
+
return dir;
|
|
118
|
+
}
|
|
119
|
+
if (fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
|
|
90
120
|
if (dir === root) break;
|
|
91
121
|
dir = path.dirname(dir);
|
|
92
122
|
}
|
|
93
|
-
return
|
|
123
|
+
return resolvedStartDir;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
function walkMarkdown(dir, out = []) {
|
|
@@ -577,6 +607,9 @@ function sidecarGuidance(root, artifactDir, activeFlowStep) {
|
|
|
577
607
|
if (state && state.next_action && normalizedStatus(state.next_action.status) !== 'done') {
|
|
578
608
|
const next = state.next_action;
|
|
579
609
|
warnings.push(`${base} next action: ${safeOneLine(next.summary)}${next.target_phase ? ` (target phase: ${safeOneLine(next.target_phase, 80)})` : ''}`);
|
|
610
|
+
if (Array.isArray(next.skills) && next.skills.length) warnings.push(`${base} required skills: ${next.skills.map(skill => safeOneLine(skill, 80)).join(' -> ')}`);
|
|
611
|
+
if (Array.isArray(next.operations) && next.operations.length) warnings.push(`${base} required operations: ${next.operations.map(operation => safeOneLine(operation, 80)).join(' -> ')}`);
|
|
612
|
+
if (next.command) warnings.push(`${base} next command: ${safeOneLine(next.command, 240)}`);
|
|
580
613
|
}
|
|
581
614
|
|
|
582
615
|
// ── Evidence verdict + checks: bundle-first, fallback to evidence.json ────
|
|
@@ -686,10 +719,58 @@ function normalizeCommand(value) {
|
|
|
686
719
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
687
720
|
}
|
|
688
721
|
|
|
722
|
+
/**
|
|
723
|
+
* #362 (iteration-2 fix item 4/LOW): single-sourced human-facing remediation phrase for an
|
|
724
|
+
* ambiguous bare grep/diff exit-1 command. Every emission site below (captureCrossReference's
|
|
725
|
+
* capture-log cross-reference branch, its live-backstop-re-run branch, and
|
|
726
|
+
* capturedFailReconciliation's ambiguous bucket) interpolates this constant rather than
|
|
727
|
+
* re-duplicating the literal string — mirrors how `isAmbiguousAbsenceCommand` itself is
|
|
728
|
+
* single-sourced (required once from ./lib/runnable-command.js, never reimplemented per call
|
|
729
|
+
* site). Cross-ref: src/cli/workflow-sidecar.ts keeps its OWN copy of the shared fragment
|
|
730
|
+
* (`AMBIGUOUS_REMEDIATION_ADVICE`) for its two record-time emission sites — the two files do not
|
|
731
|
+
* share a module for this string, so each file single-sources independently.
|
|
732
|
+
*/
|
|
733
|
+
const AMBIGUOUS_REMEDIATION = "re-record this check as a self-asserting command ('! grep ...' or 'grep -c ... | grep -qx 0')";
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* #362: a bare (non-self-asserting) grep/diff exit 1 is exit-code-AMBIGUOUS (zero
|
|
737
|
+
* matches/no diff — could be the author's intended PASS for an absence check, or an
|
|
738
|
+
* unintended miss for a presence check), not a genuine FAIL. This is the SAME narrow,
|
|
739
|
+
* two-binary-only carve-out `isAmbiguousAbsenceCommand` already applies at capture time
|
|
740
|
+
* (`evidence-capture.js`'s `observeResult`) — reused here (not reimplemented) so a log
|
|
741
|
+
* entry's `observedResult` and this re-derivation from raw exitCode can never disagree.
|
|
742
|
+
* Exit codes >=2 for grep/diff remain hard FAILs (real tool errors) — this carve-out is
|
|
743
|
+
* exactly exitCode === 1, nothing wider.
|
|
744
|
+
*
|
|
745
|
+
* Iteration-2 (re-plan) finding #3 (HIGH): `ambiguous` now has TWO distinct origins, threaded
|
|
746
|
+
* via the additive `absenceAmbiguous` discriminator so existing #362 callers are unaffected:
|
|
747
|
+
* - absence-ambiguous (#362 carve-out): `exitCode===1` on a bare grep/diff — the command
|
|
748
|
+
* genuinely RAN and produced exit 1. `absenceAmbiguous:true`.
|
|
749
|
+
* - no-signal-ambiguous (rule-3 default): `observedResult==='ambiguous'` with no exit code
|
|
750
|
+
* recoverable (e.g. an unreadable codex host banner) — capture saw the command run but got
|
|
751
|
+
* NO usable pass/fail signal. `absenceAmbiguous:false`.
|
|
752
|
+
* Callers that need the #362-specific grep/diff wording/HARD_BLOCK branch on `absenceAmbiguous`;
|
|
753
|
+
* callers that just need "was this ambiguous at all" keep using `ambiguous`.
|
|
754
|
+
*
|
|
755
|
+
* @param {{command?: string, observedResult?: string, exitCode?: number}} entry
|
|
756
|
+
* @returns {{failed: boolean, ambiguous: boolean, absenceAmbiguous: boolean}}
|
|
757
|
+
*/
|
|
758
|
+
function classifyLogEntry(entry) {
|
|
759
|
+
const exitCode = Number.isInteger(entry.exitCode) ? entry.exitCode : null;
|
|
760
|
+
const absenceAmbiguous = exitCode === 1 && isAmbiguousAbsenceCommand(entry.command);
|
|
761
|
+
const ambiguous = entry.observedResult === 'ambiguous' || absenceAmbiguous;
|
|
762
|
+
if (ambiguous) return { failed: false, ambiguous: true, absenceAmbiguous };
|
|
763
|
+
const failed = entry.observedResult === 'fail' || (exitCode !== null && exitCode !== 0);
|
|
764
|
+
return { failed, ambiguous: false, absenceAmbiguous: false };
|
|
765
|
+
}
|
|
766
|
+
|
|
689
767
|
/**
|
|
690
768
|
* Read command-log.jsonl into a map of normalized-command -> aggregate outcome.
|
|
691
769
|
* If the same command was run more than once, a single FAIL makes the aggregate
|
|
692
|
-
* a fail (a caught false-completion must not be masked by a later pass-claim).
|
|
770
|
+
* a fail (a caught false-completion must not be masked by a later pass-claim). A
|
|
771
|
+
* bare grep/diff exit-1 (#362) is carved out as `ambiguous` rather than `failed` — it
|
|
772
|
+
* is surfaced as a distinct warning class by callers, never silently dropped, and
|
|
773
|
+
* never treated as a hard block.
|
|
693
774
|
*/
|
|
694
775
|
function readCommandLog(artifactDir) {
|
|
695
776
|
const file = path.join(artifactDir, 'command-log.jsonl');
|
|
@@ -704,11 +785,13 @@ function readCommandLog(artifactDir) {
|
|
|
704
785
|
if (!entry || typeof entry.command !== 'string') continue;
|
|
705
786
|
const key = normalizeCommand(entry.command);
|
|
706
787
|
if (!key) continue;
|
|
707
|
-
const failed
|
|
788
|
+
const { failed, ambiguous, absenceAmbiguous } = classifyLogEntry(entry);
|
|
708
789
|
const prev = byCommand.get(key);
|
|
709
790
|
byCommand.set(key, {
|
|
710
791
|
ran: true,
|
|
711
792
|
failed: failed || (prev ? prev.failed : false),
|
|
793
|
+
ambiguous: !failed && (ambiguous || (prev ? prev.ambiguous : false)),
|
|
794
|
+
absenceAmbiguous: !failed && (absenceAmbiguous || (prev ? prev.absenceAmbiguous : false)),
|
|
712
795
|
exitCode: Number.isInteger(entry.exitCode) ? entry.exitCode : (prev ? prev.exitCode : null),
|
|
713
796
|
});
|
|
714
797
|
}
|
|
@@ -720,7 +803,9 @@ function readCommandLog(artifactDir) {
|
|
|
720
803
|
* The LAST entry for each command wins (unlike readCommandLog which makes FAIL sticky).
|
|
721
804
|
* Used for both capturedFailReconciliation and captureCrossReference (Fix C): we want to
|
|
722
805
|
* know the LAST result, so a genuine re-run-to-pass clears the earlier FAIL. Only an actual
|
|
723
|
-
* re-run (new PASS entry in the log) clears it — a new claim cannot change the log.
|
|
806
|
+
* re-run (new PASS entry in the log) clears it — a new claim cannot change the log. #362: a
|
|
807
|
+
* bare grep/diff exit-1 is carved out as `ambiguous` (via the shared `classifyLogEntry`), not
|
|
808
|
+
* `failed` — same rationale as `readCommandLog` above.
|
|
724
809
|
*/
|
|
725
810
|
function readLatestCommandLog(artifactDir) {
|
|
726
811
|
const file = path.join(artifactDir, 'command-log.jsonl');
|
|
@@ -735,11 +820,13 @@ function readLatestCommandLog(artifactDir) {
|
|
|
735
820
|
if (!entry || typeof entry.command !== 'string') continue;
|
|
736
821
|
const key = normalizeCommand(entry.command);
|
|
737
822
|
if (!key) continue;
|
|
738
|
-
const failed
|
|
739
|
-
// LAST entry wins — genuine re-run-to-pass overwrites the earlier FAIL.
|
|
823
|
+
const { failed, ambiguous, absenceAmbiguous } = classifyLogEntry(entry);
|
|
824
|
+
// LAST entry wins — genuine re-run-to-pass overwrites the earlier FAIL/ambiguous.
|
|
740
825
|
byCommand.set(key, {
|
|
741
826
|
ran: true,
|
|
742
827
|
failed,
|
|
828
|
+
ambiguous,
|
|
829
|
+
absenceAmbiguous,
|
|
743
830
|
exitCode: Number.isInteger(entry.exitCode) ? entry.exitCode : null,
|
|
744
831
|
});
|
|
745
832
|
}
|
|
@@ -915,28 +1002,8 @@ function verifyCommandLogChain(artifactDir) {
|
|
|
915
1002
|
* FLOW_AGENTS_GOAL_FIT_RECHECK=true (the RCE-risky opt-in path).
|
|
916
1003
|
* Returns { argv, cwd, source } or null when nothing trusted resolves.
|
|
917
1004
|
*/
|
|
918
|
-
// WS8 (AC10b):
|
|
919
|
-
//
|
|
920
|
-
// rejects prose so the goal-fit backstop never spawns `bash -lc "<a sentence>"` and
|
|
921
|
-
// misreports the resulting shell error as a caught false-completion. Pairs with the rule
|
|
922
|
-
// stated in context/contracts/planning-contract.md (AC11).
|
|
923
|
-
function isRunnableCommandText(text) {
|
|
924
|
-
const s = typeof text === 'string' ? text.trim() : '';
|
|
925
|
-
if (!s) return false;
|
|
926
|
-
// Sentence-like prose (a . ! or ? followed by more words) is not a single command.
|
|
927
|
-
if (/[.!?]\s+\S/.test(s)) return false;
|
|
928
|
-
const first = s.split(/\s+/)[0] || '';
|
|
929
|
-
// WS8 (AC10b, iteration 2): the first-token allowlist is broadened to the common
|
|
930
|
-
// real-world verify binaries a project's runnable evidence names (git, tsc, eslint,
|
|
931
|
-
// python/python3, docker, curl, jq, diff, grep, test) so honest command evidence is not
|
|
932
|
-
// misclassified as prose. Still fail-closed for genuinely non-command prose: a bare
|
|
933
|
-
// sentence whose first token is none of these (and carries no path prefix or shell
|
|
934
|
-
// metacharacter) is rejected rather than executed.
|
|
935
|
-
const knownBinary = /^(npm|npx|pnpm|yarn|node|bash|sh|zsh|make|just|task|cargo|go|pytest|tox|deno|bun|ruby|rake|mvn|gradle|dotnet|swift|flutter|dart|git|tsc|eslint|python|python3|docker|curl|jq|diff|grep|test)$/.test(first)
|
|
936
|
-
|| first.startsWith('./') || first.startsWith('/') || first.includes('/');
|
|
937
|
-
const hasShellMeta = /[|&;<>()$`]/.test(s);
|
|
938
|
-
return knownBinary || hasShellMeta;
|
|
939
|
-
}
|
|
1005
|
+
// WS8 (AC10b): isRunnableCommandText now lives in ./lib/runnable-command.js (single
|
|
1006
|
+
// source of truth shared with workflow-sidecar.ts's record-time validation, #412).
|
|
940
1007
|
|
|
941
1008
|
function resolveTrustedCommand(root, artifactDir, check, acceptance) {
|
|
942
1009
|
// (a) acceptance criterion command for the matching criterion.
|
|
@@ -946,6 +1013,32 @@ function resolveTrustedCommand(root, artifactDir, check, acceptance) {
|
|
|
946
1013
|
// is not a runnable shell command is malformed-evidence — reported distinctly, not
|
|
947
1014
|
// executed, and not conflated with a caught false-completion.
|
|
948
1015
|
if (!isRunnableCommandText(fromAcceptance)) return { malformed: fromAcceptance };
|
|
1016
|
+
// DIALECT-PRESERVATION INVARIANT (#362): from THIS POINT ONWARD, `fromAcceptance` MUST be
|
|
1017
|
+
// passed through to argv[2] unmodified from `acceptanceCommandFor`'s return value — never
|
|
1018
|
+
// re-trimmed, re-escaped, re-quoted, or reconstructed from a parsed/tokenized form. The
|
|
1019
|
+
// recorded command's regex dialect and flags (e.g. `grep -E '(foo|bar)'`'s ERE alternation/
|
|
1020
|
+
// grouping, or any other -E/-P/-G flag combination) are part of the evidence-ref text itself;
|
|
1021
|
+
// re-deriving the command string from anything other than this exact `fromAcceptance` value
|
|
1022
|
+
// would silently reinterpret an ERE-recorded pattern as BRE (or vice versa) on re-run,
|
|
1023
|
+
// corrupting the very check this backstop exists to confirm.
|
|
1024
|
+
//
|
|
1025
|
+
// KNOWN ACCEPTED UPSTREAM BOUNDARY: this guarantee begins at `acceptanceCommandFor`'s return
|
|
1026
|
+
// value, NOT at the raw evidence-ref text — it is NOT "byte-for-byte, exactly as recorded in
|
|
1027
|
+
// the evidence ref" (a prior version of this comment overclaimed that). `acceptanceCommandFor`
|
|
1028
|
+
// derives `fromAcceptance` via `normalizeCommand` (~line 686: `.replace(/\s+/g, ' ').trim()`),
|
|
1029
|
+
// which collapses whitespace runs and trims BEFORE this invariant's scope begins. Regex
|
|
1030
|
+
// dialect metacharacters (-E, |, +, (), etc.) are single characters/short tokens, not
|
|
1031
|
+
// whitespace, so they are untouched by this collapse and survive intact — but a literal
|
|
1032
|
+
// multi-space or tab run *inside* a recorded pattern (e.g. two spaces in `grep -E 'foo bar'`)
|
|
1033
|
+
// does NOT survive verbatim; it is already collapsed to one space by the time it reaches this
|
|
1034
|
+
// function. That is a pre-existing, accepted normalization boundary (not introduced by this
|
|
1035
|
+
// invariant, and not something this invariant claims to cover) — not a dialect-preservation
|
|
1036
|
+
// gap.
|
|
1037
|
+
// `runBackstop` (below) preserves this invariant on its side by reading `trusted.argv[2]`
|
|
1038
|
+
// verbatim rather than re-joining `argv` with spaces — see runBackstop's own
|
|
1039
|
+
// DIALECT-PRESERVATION INVARIANT comment for that half of the contract.
|
|
1040
|
+
// A regression eval (evals/integration/test_goal_fit_hook.sh, "#362 dialect preservation")
|
|
1041
|
+
// pins this end-to-end with a `grep -E` ERE-only alternation construct.
|
|
949
1042
|
return { argv: ['bash', '-lc', fromAcceptance], cwd: root, source: 'acceptance' };
|
|
950
1043
|
}
|
|
951
1044
|
|
|
@@ -1045,6 +1138,33 @@ function resolveBackstopMode() {
|
|
|
1045
1138
|
return 'block';
|
|
1046
1139
|
}
|
|
1047
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* #362: `classification` is a three-state result (`'pass'`/`'fail'`/`'ambiguous'`) rather than
|
|
1143
|
+
* the old boolean `passed` — `'ambiguous'` iff the re-run command text is a bare (non-negated,
|
|
1144
|
+
* non-count-asserted, non-chained) `grep`/`diff` invocation (`isAmbiguousAbsenceCommand`) AND
|
|
1145
|
+
* `exitCode === 1` (zero matches/no diff — could be the author's intended PASS for an absence
|
|
1146
|
+
* check, never silently coerced to pass OR fail). `'fail'` iff `exitCode !== 0` and not the
|
|
1147
|
+
* ambiguous case (this still includes exitCode >= 2 for grep/diff — a real tool error, e.g. a
|
|
1148
|
+
* missing file — which stays a hard fail, proving the carve-out is narrowly scoped). `'pass'`
|
|
1149
|
+
* iff `exitCode === 0`. `trusted.argv.join(' ')` is passed to `isAmbiguousAbsenceCommand` (it
|
|
1150
|
+
* already unwraps a `bash -lc '<command>'` wrapper on its own, so the `['bash', '-lc', cmd]`
|
|
1151
|
+
* shape `resolveTrustedCommand`'s acceptance/model-command sources produce is inspected
|
|
1152
|
+
* correctly; a `declared`-manifest-target argv like `['npm', 'test']` simply never matches
|
|
1153
|
+
* `grep`/`diff` as its first token, so it is never misclassified ambiguous).
|
|
1154
|
+
*
|
|
1155
|
+
* DIALECT-PRESERVATION INVARIANT (#362): `commandText` below MUST be `trusted.argv[2]`'s value,
|
|
1156
|
+
* verbatim — never re-derived, re-joined, or re-quoted from `trusted.argv`. The recorded
|
|
1157
|
+
* command's exact dialect/flags (e.g. `grep -E '(foo|bar)'`'s ERE alternation) are part of the
|
|
1158
|
+
* evidence ref text; re-joining `argv` with spaces (or any other reconstruction) would silently
|
|
1159
|
+
* reintroduce a BRE/ERE dialect mismatch — the exact bug this invariant exists to prevent. This
|
|
1160
|
+
* mirrors the equivalent invariant on `resolveTrustedCommand`'s `acceptance` branch below (search
|
|
1161
|
+
* DIALECT-PRESERVATION INVARIANT there, including its accepted whitespace-collapse boundary
|
|
1162
|
+
* note) — the same recorded string must survive unmodified from `acceptanceCommandFor`'s return
|
|
1163
|
+
* value through to this spawnSync call.
|
|
1164
|
+
*
|
|
1165
|
+
* @param {{argv: string[], cwd: string}} trusted
|
|
1166
|
+
* @returns {{ran: boolean, exitCode?: number, classification?: 'pass'|'fail'|'ambiguous', error?: string, timedOut?: boolean}}
|
|
1167
|
+
*/
|
|
1048
1168
|
function runBackstop(trusted) {
|
|
1049
1169
|
const result = spawnSync(trusted.argv[0], trusted.argv.slice(1), {
|
|
1050
1170
|
cwd: trusted.cwd,
|
|
@@ -1055,7 +1175,19 @@ function runBackstop(trusted) {
|
|
|
1055
1175
|
});
|
|
1056
1176
|
if (result.error) return { ran: false, error: result.error.code || result.error.message };
|
|
1057
1177
|
if (result.signal) return { ran: false, error: `killed (${result.signal})`, timedOut: result.signal === 'SIGKILL' || result.signal === 'SIGTERM' };
|
|
1058
|
-
|
|
1178
|
+
const exitCode = result.status;
|
|
1179
|
+
// The `['bash', '-lc', <command-text>]` shape resolveTrustedCommand's acceptance/
|
|
1180
|
+
// model-command sources produce carries the ORIGINAL recorded command text verbatim in
|
|
1181
|
+
// argv[2] — pass THAT (not a re-joined/re-quoted `argv.join(' ')`, which would lose the
|
|
1182
|
+
// shell-quoting isAmbiguousAbsenceCommand's `bash -lc '...'` unwrap regex requires) to the
|
|
1183
|
+
// heuristic. A `declared`-manifest-target argv (e.g. `['npm', 'test']`) has no such wrapper;
|
|
1184
|
+
// joining it is fine since its first token is never `grep`/`diff` anyway.
|
|
1185
|
+
const commandText = (trusted.argv.length === 3 && trusted.argv[0] === 'bash' && trusted.argv[1] === '-lc')
|
|
1186
|
+
? trusted.argv[2]
|
|
1187
|
+
: trusted.argv.join(' ');
|
|
1188
|
+
const ambiguous = exitCode === 1 && isAmbiguousAbsenceCommand(commandText);
|
|
1189
|
+
const classification = ambiguous ? 'ambiguous' : (exitCode === 0 ? 'pass' : 'fail');
|
|
1190
|
+
return { ran: true, exitCode, classification };
|
|
1059
1191
|
}
|
|
1060
1192
|
|
|
1061
1193
|
/**
|
|
@@ -1081,6 +1213,8 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1081
1213
|
const base = relative(root, artifactDir);
|
|
1082
1214
|
const backstopMode = resolveBackstopMode();
|
|
1083
1215
|
const warnings = [];
|
|
1216
|
+
const captureState = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
1217
|
+
const terminalDelivered = isTerminalDeliveredState(captureState);
|
|
1084
1218
|
|
|
1085
1219
|
// AC3 fail-closed: detect a missing command log in a post-execution session.
|
|
1086
1220
|
// When state.json confirms the session is past the planning phase (commands should
|
|
@@ -1094,7 +1228,6 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1094
1228
|
{
|
|
1095
1229
|
const logFileMissing = !fs.existsSync(path.join(artifactDir, 'command-log.jsonl'));
|
|
1096
1230
|
if (logFileMissing) {
|
|
1097
|
-
const captureState = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
1098
1231
|
if (captureState) {
|
|
1099
1232
|
const capturePhase = normalizedStatus(captureState.phase || '');
|
|
1100
1233
|
const captureStatus = normalizedStatus(captureState.status || '');
|
|
@@ -1194,6 +1327,23 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1194
1327
|
if (logged.failed) {
|
|
1195
1328
|
const exit = Number.isInteger(logged.exitCode) ? ` (exitCode:${logged.exitCode})` : '';
|
|
1196
1329
|
warnings.push(`${base} evidence check ${id}: capture log CONTRADICTS claimed pass — command "${safeOneLine(cmd, 120)}" was recorded as FAIL${exit}. This is a caught false-completion.`);
|
|
1330
|
+
} else if (logged.ambiguous && logged.absenceAmbiguous) {
|
|
1331
|
+
// #362: a bare grep/diff logged exit 1 — ambiguous (zero matches/no diff), not a
|
|
1332
|
+
// caught false-completion. Distinct warning class, never silently dropped, never a
|
|
1333
|
+
// hard block. UNCHANGED (byte-identical) — iteration-2 (re-plan) finding #3 Decision A
|
|
1334
|
+
// keeps this absence-ambiguous branch verbatim; only the sibling no-signal branch below
|
|
1335
|
+
// is new.
|
|
1336
|
+
const exit = Number.isInteger(logged.exitCode) ? ` (exitCode:${logged.exitCode})` : '';
|
|
1337
|
+
warnings.push(`${base} evidence check ${id}: capture log shows command "${safeOneLine(cmd, 120)}" exited 1${exit} — for bare grep/diff this may mean zero matches/no differences (PASS for an absence check) or an unintended miss (FAIL for a presence check); NOT_VERIFIED (ambiguous): ${AMBIGUOUS_REMEDIATION} to remove the ambiguity.`);
|
|
1338
|
+
} else if (logged.ambiguous) {
|
|
1339
|
+
// Iteration-2 (re-plan) finding #3 (HIGH), Decision A: generic no-signal-ambiguous — the
|
|
1340
|
+
// rule-3 default (`observedResult:'ambiguous'`, no exit code recoverable, e.g. an
|
|
1341
|
+
// unreadable/missing codex host banner). This is NOT the #362 grep/diff carve-out (no
|
|
1342
|
+
// `absenceAmbiguous`), so the message is grep/diff-FREE and accurate for any command.
|
|
1343
|
+
// Deliberately does NOT contain `NOT_VERIFIED (ambiguous)`, `caught false-completion`, or
|
|
1344
|
+
// `status:` so it MISSES HARD_BLOCK (warn-only on a terminal/pre-execution stop) while
|
|
1345
|
+
// `evidence check` / `NOT_VERIFIED —` still match FULL_BLOCK (blocks a non-terminal stop).
|
|
1346
|
+
warnings.push(`${base} evidence check ${id}: claimed pass but NOT_VERIFIED — command "${safeOneLine(cmd, 120)}" was captured with no deterministic pass/fail signal (no exit code observed on this host); a pass requires positive evidence. Re-run it to produce a definitive exit code, or record it as not_verified/accepted_gap.`);
|
|
1197
1347
|
} else if (hasLaunderingOperator(cmd)) {
|
|
1198
1348
|
// Fix D: exit-code laundering. The captured exit-0 is not trustworthy — the command
|
|
1199
1349
|
// baked in '|| true' / '|| :' / '; true' / '; exit 0' / '| true' to mask the real result.
|
|
@@ -1219,17 +1369,30 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1219
1369
|
warnings.push(`${base} evidence check ${id}: claimed pass but NOT_VERIFIED — command "${safeOneLine(cmd, 120)}" was never captured and no trusted command (acceptance criterion / declared manifest target) resolves to re-run it. Set FLOW_AGENTS_GOAL_FIT_RECHECK=true to opt into re-running the model's free-form command.`);
|
|
1220
1370
|
continue;
|
|
1221
1371
|
}
|
|
1372
|
+
if (terminalDelivered && trusted.source === 'model-command (FLOW_AGENTS_GOAL_FIT_RECHECK)') {
|
|
1373
|
+
// Terminal delivered/done sessions have finalized evidence. Do not turn
|
|
1374
|
+
// model-asserted command text into a new RECHECK false-completion. Captured
|
|
1375
|
+
// execution evidence and CI/L2 checks remain the anchors for terminal tasks.
|
|
1376
|
+
warnings.push(`${base} evidence check ${id}: malformed-evidence — terminal delivered/done session recorded model-command text asserted by FLOW_AGENTS_GOAL_FIT_RECHECK ("${safeOneLine(cmd, 120)}"); it was NOT re-run on a terminal session. Captured-execution evidence and CI/L2 checks remain the anchors.`);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1222
1379
|
const outcome = runBackstop(trusted);
|
|
1223
1380
|
if (!outcome.ran) {
|
|
1224
1381
|
warnings.push(`${base} evidence check ${id}: claimed pass but NOT_VERIFIED — trusted backstop (${trusted.source}) could not run (${safeOneLine(outcome.error, 80)}).`);
|
|
1225
1382
|
continue;
|
|
1226
1383
|
}
|
|
1227
|
-
if (
|
|
1384
|
+
if (outcome.classification === 'ambiguous') {
|
|
1385
|
+
// #362: bare grep/diff backstop re-run exited 1 — ambiguous, not a caught
|
|
1386
|
+
// false-completion. Never silently PASS (a presence check recorded without
|
|
1387
|
+
// negation could genuinely be a miss) and never silently dropped.
|
|
1388
|
+
const note = `${base} evidence check ${id}: trusted backstop (${trusted.source}) re-run of "${trusted.argv.join(' ')}" exited 1 — for grep/diff this may mean zero matches/no differences (PASS for an absence check) or an unintended miss (FAIL for a presence check); NOT_VERIFIED (ambiguous): ${AMBIGUOUS_REMEDIATION} to remove the ambiguity.`;
|
|
1389
|
+
warnings.push(note);
|
|
1390
|
+
} else if (outcome.classification === 'fail') {
|
|
1228
1391
|
const note = `${base} evidence check ${id}: trusted backstop (${trusted.source}) re-run of "${trusted.argv.join(' ')}" FAILED with exit ${outcome.exitCode}, contradicting the claimed pass. This is a caught false-completion.`;
|
|
1229
1392
|
if (backstopMode === 'off') warnings.push(`${note} [backstop in warn mode — not blocking]`);
|
|
1230
1393
|
else warnings.push(note);
|
|
1231
1394
|
}
|
|
1232
|
-
// backstop
|
|
1395
|
+
// backstop classification 'pass' → claim deterministically confirmed by re-run, no warning.
|
|
1233
1396
|
}
|
|
1234
1397
|
|
|
1235
1398
|
return warnings;
|
|
@@ -1289,7 +1452,18 @@ function capturedFailReconciliation(root, artifactDir, taskStatus) {
|
|
|
1289
1452
|
if (!info.failed && hasLaunderingOperator(cmd)) launderedPass.set(cmd, info);
|
|
1290
1453
|
}
|
|
1291
1454
|
|
|
1292
|
-
|
|
1455
|
+
// Iteration-2 fix item 4 (HIGH, ambiguity-must-not-slip-terminal-stops): a THIRD bucket —
|
|
1456
|
+
// commands whose LATEST capture is `ambiguous` (a bare, non-self-asserting grep/diff exit 1;
|
|
1457
|
+
// #362) — collected separately from `latestFails`, never merged into it. An ambiguous capture
|
|
1458
|
+
// is NOT a caught false-completion (it may genuinely be the author's intended pass for an
|
|
1459
|
+
// absence check) — it gets its own distinct re-record-self-asserting message below, never the
|
|
1460
|
+
// "caught false-completion" accusation `latestFails`/`launderedPass` use.
|
|
1461
|
+
const latestAmbiguous = new Map(); // cmd -> {failed:false, ambiguous:true, exitCode}
|
|
1462
|
+
for (const [cmd, info] of latestLog) {
|
|
1463
|
+
if (!info.failed && info.ambiguous) latestAmbiguous.set(cmd, info);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (latestFails.size === 0 && launderedPass.size === 0 && latestAmbiguous.size === 0) return []; // Nothing to flag
|
|
1293
1467
|
|
|
1294
1468
|
const base = relative(root, artifactDir);
|
|
1295
1469
|
const warnings = [];
|
|
@@ -1305,8 +1479,8 @@ function capturedFailReconciliation(root, artifactDir, taskStatus) {
|
|
|
1305
1479
|
if (c && c.id) claimById.set(c.id, c);
|
|
1306
1480
|
}
|
|
1307
1481
|
|
|
1308
|
-
// commandsToCheck: FAIL-latest commands + laundered-pass commands
|
|
1309
|
-
const commandsToCheck = new Set([...latestFails.keys(), ...launderedPass.keys()]);
|
|
1482
|
+
// commandsToCheck: FAIL-latest commands + laundered-pass commands + ambiguous-latest commands
|
|
1483
|
+
const commandsToCheck = new Set([...latestFails.keys(), ...launderedPass.keys(), ...latestAmbiguous.keys()]);
|
|
1310
1484
|
|
|
1311
1485
|
// For each relevant command, track what claims say about it.
|
|
1312
1486
|
// cmdAcc: cmd → {passClaims: [...], ackClaims: []}
|
|
@@ -1377,6 +1551,43 @@ function capturedFailReconciliation(root, artifactDir, taskStatus) {
|
|
|
1377
1551
|
}
|
|
1378
1552
|
}
|
|
1379
1553
|
|
|
1554
|
+
// Iteration-2 fix item 4 (HIGH): THIRD bucket — commands whose latest capture is `ambiguous`
|
|
1555
|
+
// AND some claim asserts pass for them. This is deliberately gated on `acc.passClaims.length >
|
|
1556
|
+
// 0` (mirroring Case A / Fix D above) — an ambiguous capture with NO pass-claim at all is not
|
|
1557
|
+
// reconciled here (nothing was asserted against it to reconcile), matching the existing
|
|
1558
|
+
// no-over-block guarantee for incidental runs. When a claim DOES assert pass for an ambiguous
|
|
1559
|
+
// command, this is a DISTINCT re-record-self-asserting message — NEVER the "caught
|
|
1560
|
+
// false-completion" accusation Case A/Fix D use, since a bare grep/diff exit 1 may genuinely be
|
|
1561
|
+
// the author's intended pass (zero matches/no differences for an absence check). This bucket
|
|
1562
|
+
// still BLOCKS a terminal stop (via HARD_BLOCK's dedicated ambiguous pattern below) because the
|
|
1563
|
+
// claim is asserting something the capture cannot actually confirm — recording is not the same
|
|
1564
|
+
// as verified completion.
|
|
1565
|
+
for (const [cmd, ambiguousInfo] of latestAmbiguous) {
|
|
1566
|
+
const exit = Number.isInteger(ambiguousInfo.exitCode) ? ambiguousInfo.exitCode : null;
|
|
1567
|
+
const exitStr = exit !== null ? ` (exit ${exit})` : '';
|
|
1568
|
+
const acc = cmdAcc.get(cmd);
|
|
1569
|
+
if (acc && acc.passClaims.length > 0) {
|
|
1570
|
+
const claim = acc.passClaims[0];
|
|
1571
|
+
if (ambiguousInfo.absenceAmbiguous) {
|
|
1572
|
+
// #362 absence-ambiguous — UNCHANGED (byte-identical) message + HARD_BLOCK marker.
|
|
1573
|
+
warnings.push(
|
|
1574
|
+
`${base} captured command '${safeOneLine(cmd, 120)}' last ran AMBIGUOUS${exitStr} ` +
|
|
1575
|
+
`(bare grep/diff exit 1 — zero matches/no differences) and claim ${safeOneLine(claim.subjectId || claim.id, 80)} ` +
|
|
1576
|
+
`(${safeOneLine(claim.claimType, 48)}) asserts pass — NOT_VERIFIED (ambiguous): ${AMBIGUOUS_REMEDIATION} and re-run it, then re-record the claim.`
|
|
1577
|
+
);
|
|
1578
|
+
} else {
|
|
1579
|
+
// Iteration-2 (re-plan) finding #3 Decision A: generic no-signal-ambiguous (no exit code
|
|
1580
|
+
// ever recoverable) — grep/diff-FREE wording, misses HARD_BLOCK, still matches FULL_BLOCK
|
|
1581
|
+
// via `NOT_VERIFIED —` so a non-terminal stop still blocks.
|
|
1582
|
+
warnings.push(
|
|
1583
|
+
`${base} captured command '${safeOneLine(cmd, 120)}' last ran with no deterministic pass/fail signal${exitStr} ` +
|
|
1584
|
+
`(no exit code observed on this host) and claim ${safeOneLine(claim.subjectId || claim.id, 80)} ` +
|
|
1585
|
+
`(${safeOneLine(claim.claimType, 48)}) asserts pass — NOT_VERIFIED — re-run it to produce a definitive exit code, then re-record the claim.`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1380
1591
|
return warnings;
|
|
1381
1592
|
}
|
|
1382
1593
|
|
|
@@ -1639,16 +1850,30 @@ function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
|
|
|
1639
1850
|
//
|
|
1640
1851
|
// HARD_BLOCK: always blocks, even for pre-execution and terminal tasks.
|
|
1641
1852
|
// Fires on genuine false-completion signals (a claimed pass the capture log or
|
|
1642
|
-
// evidence.json contradicts), integrity failures, and
|
|
1853
|
+
// evidence.json contradicts), integrity failures, gate misconfiguration, and — iteration-2 fix
|
|
1854
|
+
// item 4 (HIGH) — the unified ambiguous-with-a-pass-claim signal (`NOT_VERIFIED (ambiguous)`,
|
|
1855
|
+
// built from the shared AMBIGUOUS_REMEDIATION const). ALL THREE emission sites — the capture-log
|
|
1856
|
+
// shortcut in captureCrossReference, the live re-run in runBackstop, and the THIRD bucket in
|
|
1857
|
+
// capturedFailReconciliation — emit this same marker, and it is DELIBERATELY distinct text from
|
|
1858
|
+
// `caught false-completion` (never the false-completion accusation for a bare grep/diff exit 1,
|
|
1859
|
+
// which may genuinely be the author's intended pass). All three DELIBERATELY match HARD_BLOCK,
|
|
1860
|
+
// so an ambiguous claimed-pass BLOCKS a terminal/pre-execution stop in every config
|
|
1861
|
+
// (evidence.json-only, never-captured, and captured+bundle) — a claim asserting pass for a
|
|
1862
|
+
// command whose exit code cannot actually confirm that is not a verified completion either. Do
|
|
1863
|
+
// NOT revert any of the three sites back to plain `NOT_VERIFIED —` text: that would desync them
|
|
1864
|
+
// from HARD_BLOCK and reopen the terminal-stop ambiguous bypass. (The genuinely non-ambiguous
|
|
1865
|
+
// `NOT_VERIFIED —` cases — never-captured, backstop-disabled, no-trust-bundle, etc. — are a
|
|
1866
|
+
// separate, unrelated class: they stay warn-only for a terminal/pre-execution stop and only
|
|
1867
|
+
// block a non-terminal stop via FULL_BLOCK's broader `NOT_VERIFIED —` pattern, unchanged.)
|
|
1643
1868
|
//
|
|
1644
1869
|
// FULL_BLOCK: fires for execution-onward tasks (post-planning, non-terminal).
|
|
1645
1870
|
// Includes all HARD_BLOCK patterns plus completeness/hygiene and not-done state.
|
|
1646
1871
|
//
|
|
1647
1872
|
// Both are used in analyze() for blocking decisions AND in run() for the AC2
|
|
1648
1873
|
// MAX_BLOCKS hard-block guard (preventing auto-release of hard blocks).
|
|
1649
|
-
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered/;
|
|
1874
|
+
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered|NOT_VERIFIED \(ambiguous\)/;
|
|
1650
1875
|
// FULL_BLOCK adds: workflow-state hygiene, surface-unavailable fail-closed, missing log.
|
|
1651
|
-
const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered|malformed-evidence/;
|
|
1876
|
+
const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered|malformed-evidence|NOT_VERIFIED \(ambiguous\)/;
|
|
1652
1877
|
|
|
1653
1878
|
async function analyze(root, now = Date.now()) {
|
|
1654
1879
|
const flowAgentsDirs = flowAgentsArtifactRootsForRead(root);
|
|
@@ -1740,7 +1965,8 @@ async function analyze(root, now = Date.now()) {
|
|
|
1740
1965
|
// Use module-scope HARD_BLOCK / FULL_BLOCK (defined above analyze()).
|
|
1741
1966
|
// pre-execution/terminal tasks: only HARD_BLOCK signals cause a block.
|
|
1742
1967
|
// execution-onward tasks: FULL_BLOCK signals cause a block.
|
|
1743
|
-
const
|
|
1968
|
+
const activeFlowRun = gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active';
|
|
1969
|
+
const blockRe = ((preExecution && !activeFlowRun) || terminal) ? HARD_BLOCK : FULL_BLOCK;
|
|
1744
1970
|
const blocking = warnings.some(w => {
|
|
1745
1971
|
// Capture cross-reference warn-mode notes never block (operator opted out).
|
|
1746
1972
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
@@ -1994,6 +2220,10 @@ function releaseOnNonTerminalStop(root, artifactDir) {
|
|
|
1994
2220
|
|
|
1995
2221
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
1996
2222
|
if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
|
|
2223
|
+
if (state.flow_run && normalizedStatus(state.flow_run.status) === 'active') {
|
|
2224
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for active Flow run "${safeOneLine(state.flow_run.run_id || state.task_slug || 'unknown', 80)}"; continuation remains governed by Flow state.\n`);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
1997
2227
|
|
|
1998
2228
|
const sidecar = loadWorkflowSidecarBuilt();
|
|
1999
2229
|
if (!sidecar) {
|