@kontourai/flow-agents 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +16 -0
- package/.github/workflows/ci.yml +44 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +62 -0
- package/CONTEXT.md +35 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +27 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +938 -168
- package/build/src/cli.js +6 -0
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +24 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +40 -7
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
- package/context/contracts/decision-registry-contract.md +265 -0
- package/context/contracts/delivery-contract.md +3 -1
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +4 -1
- package/context/contracts/review-contract.md +7 -3
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +11 -1
- package/context/contracts/work-item-contract.md +3 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +10 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +17 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +4 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +3 -2
- package/docs/workflow-usage-guide.md +260 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +71 -1
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +23 -6
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +37 -13
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +11 -10
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +32 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +32 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +148 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +11 -5
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +957 -103
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +27 -7
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +933 -168
- package/src/cli.ts +6 -0
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +24 -1
|
@@ -4,18 +4,20 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const KONTOURAI_DIR = '.kontourai';
|
|
7
|
-
const
|
|
7
|
+
const FLOW_AGENTS_RUNTIME_SUBDIR = 'flow-agents';
|
|
8
|
+
const FLOW_AGENTS_RUNTIME_DIR = `${KONTOURAI_DIR}/${FLOW_AGENTS_RUNTIME_SUBDIR}`;
|
|
9
|
+
const DURABLE_FLOW_AGENTS_DIR = '.flow-agents';
|
|
8
10
|
|
|
9
11
|
function flowAgentsArtifactRoot(cwd = process.cwd()) {
|
|
10
|
-
return path.resolve(cwd,
|
|
12
|
+
return path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function
|
|
14
|
-
return path.resolve(cwd,
|
|
15
|
+
function durableFlowAgentsRoot(cwd = process.cwd()) {
|
|
16
|
+
return path.resolve(cwd, DURABLE_FLOW_AGENTS_DIR);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function flowAgentsArtifactRootsForRead(cwd = process.cwd()) {
|
|
18
|
-
const roots = [flowAgentsArtifactRoot(cwd)
|
|
20
|
+
const roots = [flowAgentsArtifactRoot(cwd)];
|
|
19
21
|
return roots.filter((root, index) => roots.indexOf(root) === index && fs.existsSync(root));
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -25,8 +27,12 @@ function defaultArtifactRootForRead(cwd = process.cwd()) {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
module.exports = {
|
|
30
|
+
DURABLE_FLOW_AGENTS_DIR,
|
|
31
|
+
FLOW_AGENTS_RUNTIME_DIR,
|
|
32
|
+
FLOW_AGENTS_RUNTIME_SUBDIR,
|
|
33
|
+
KONTOURAI_DIR,
|
|
34
|
+
durableFlowAgentsRoot,
|
|
28
35
|
flowAgentsArtifactRoot,
|
|
29
|
-
legacyFlowAgentsArtifactRoot,
|
|
30
36
|
flowAgentsArtifactRootsForRead,
|
|
31
37
|
defaultArtifactRootForRead,
|
|
32
38
|
};
|
|
@@ -66,9 +66,35 @@ async function main() {
|
|
|
66
66
|
const [, , eventArg = 'unknown', agentName = 'dev'] = process.argv;
|
|
67
67
|
const raw = await readStdinRaw();
|
|
68
68
|
const payload = parseJson(raw);
|
|
69
|
+
const canonical = canonicalEvent(eventArg, payload);
|
|
69
70
|
const telemetryScript = path.resolve(__dirname, '..', 'telemetry', 'telemetry.sh');
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
if (canonical === 'postToolUse') {
|
|
73
|
+
try {
|
|
74
|
+
const heartbeatResult = require('./lib/liveness-heartbeat').maybeEmitHeartbeat({
|
|
75
|
+
cwd: process.cwd(),
|
|
76
|
+
env: process.env,
|
|
77
|
+
});
|
|
78
|
+
const conflict = heartbeatResult && heartbeatResult.conflict;
|
|
79
|
+
// Degraded-runtime conflict surfacing (issue #320, AC4): this runtime's generated
|
|
80
|
+
// plugin/extension does not consume this wrapper's stdout for context injection today
|
|
81
|
+
// (confirmed by reading the generated bundle's tool-result handler) — a disclosed,
|
|
82
|
+
// pre-existing gap (see plan Unresolved Questions), not a silent one. Until that
|
|
83
|
+
// structural gap is closed, emit a stderr diagnostic only; the next-turn
|
|
84
|
+
// workflow-steering.js liveness warning remains the fallback corrector. Guarded on a
|
|
85
|
+
// well-formed `conflict` shape so a malformed value degrades to no diagnostic, never a
|
|
86
|
+
// thrown error (AC8, fail-open).
|
|
87
|
+
if (conflict && typeof conflict.actor === 'string' && typeof conflict.lastAt === 'string') {
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`[OpencodeTelemetryHook] liveness conflict: actor "${conflict.actor}" claimed this subject at "${conflict.lastAt}" — no mid-turn injection available on this runtime (see docs/spec/runtime-hook-surface.md); relying on next-turn workflow-steering warning.\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
process.stderr.write(`[OpencodeTelemetryHook] liveness heartbeat error: ${err.message}\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = spawnSync('bash', [telemetryScript, canonical, agentName], {
|
|
72
98
|
input: raw,
|
|
73
99
|
encoding: 'utf8',
|
|
74
100
|
cwd: process.cwd(),
|
|
@@ -70,9 +70,35 @@ async function main() {
|
|
|
70
70
|
const [, , eventArg = 'unknown', agentName = 'dev'] = process.argv;
|
|
71
71
|
const raw = await readStdinRaw();
|
|
72
72
|
const payload = parseJson(raw);
|
|
73
|
+
const canonical = canonicalEvent(eventArg, payload);
|
|
73
74
|
const telemetryScript = path.resolve(__dirname, '..', 'telemetry', 'telemetry.sh');
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
if (canonical === 'postToolUse') {
|
|
77
|
+
try {
|
|
78
|
+
const heartbeatResult = require('./lib/liveness-heartbeat').maybeEmitHeartbeat({
|
|
79
|
+
cwd: process.cwd(),
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
82
|
+
const conflict = heartbeatResult && heartbeatResult.conflict;
|
|
83
|
+
// Degraded-runtime conflict surfacing (issue #320, AC4): this runtime's generated
|
|
84
|
+
// plugin/extension does not consume this wrapper's stdout for context injection today
|
|
85
|
+
// (confirmed by reading the generated bundle's tool-result handler) — a disclosed,
|
|
86
|
+
// pre-existing gap (see plan Unresolved Questions), not a silent one. Until that
|
|
87
|
+
// structural gap is closed, emit a stderr diagnostic only; the next-turn
|
|
88
|
+
// workflow-steering.js liveness warning remains the fallback corrector. Guarded on a
|
|
89
|
+
// well-formed `conflict` shape so a malformed value degrades to no diagnostic, never a
|
|
90
|
+
// thrown error (AC8, fail-open).
|
|
91
|
+
if (conflict && typeof conflict.actor === 'string' && typeof conflict.lastAt === 'string') {
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`[PiTelemetryHook] liveness conflict: actor "${conflict.actor}" claimed this subject at "${conflict.lastAt}" — no mid-turn injection available on this runtime (see docs/spec/runtime-hook-surface.md); relying on next-turn workflow-steering warning.\n`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
process.stderr.write(`[PiTelemetryHook] liveness heartbeat error: ${err.message}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = spawnSync('bash', [telemetryScript, canonical, agentName], {
|
|
76
102
|
input: raw,
|
|
77
103
|
encoding: 'utf8',
|
|
78
104
|
cwd: process.cwd(),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Stop Hook: warn when an active workflow is about to stop short of its goal.
|
|
4
4
|
*
|
|
5
|
-
* The hook reads .flow-agents artifacts, looks for the most recent active
|
|
5
|
+
* The hook reads .kontourai/flow-agents artifacts, looks for the most recent active
|
|
6
6
|
* delivery/session file, and reports missing Definition Of Done, Goal Fit, or
|
|
7
7
|
* Final Acceptance state.
|
|
8
8
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Code at L2) set block so the installed product enforces while the engine
|
|
16
16
|
* default and conformance contract stay warn.
|
|
17
17
|
*
|
|
18
|
-
* Scope: the gate evaluates the session's current task (.flow-agents/current.json)
|
|
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
20
|
* this stop. It also never hard-blocks a pre-execution (not-yet-started) task on
|
|
21
21
|
* mere incompleteness — only genuine false-completion signals (a claimed pass the
|
|
@@ -315,6 +315,30 @@ function loadActiveFlowStep(flowAgentsDir) {
|
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Message-assembly only: derive the user-facing gate identity prefix from the
|
|
320
|
+
* resolved active FlowDefinition step (loadActiveFlowStep). Produces
|
|
321
|
+
* "[<flowId>/<gateId>]" (e.g. "[builder.build/verify-gate]") when a
|
|
322
|
+
* FlowDefinition gate is active, else the generic "[stop-gate]" fallback.
|
|
323
|
+
* Purely cosmetic — never affects claim selection, HARD_BLOCK/FULL_BLOCK
|
|
324
|
+
* classification, block counting, or exit codes.
|
|
325
|
+
*/
|
|
326
|
+
function gateLabel(activeFlowStep) {
|
|
327
|
+
return activeFlowStep && activeFlowStep.flowId && activeFlowStep.gateId
|
|
328
|
+
? `[${activeFlowStep.flowId}/${activeFlowStep.gateId}]`
|
|
329
|
+
: '[stop-gate]';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Derive the declared claimType Set from an active FlowDefinition's gate expects[].
|
|
334
|
+
* Returns null when no FlowDefinition is active (callers fall back to workflow.* only).
|
|
335
|
+
*/
|
|
336
|
+
function declaredClaimTypesFor(activeFlowStep) {
|
|
337
|
+
return activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
|
|
338
|
+
? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
|
|
339
|
+
: null;
|
|
340
|
+
}
|
|
341
|
+
|
|
318
342
|
function safeOneLine(value, maxLength = 220) {
|
|
319
343
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
320
344
|
if (text.length <= maxLength) return text;
|
|
@@ -527,9 +551,7 @@ function bundlePendingCriteriaCount(claims, declaredClaimTypes) {
|
|
|
527
551
|
function sidecarGuidance(root, artifactDir, activeFlowStep) {
|
|
528
552
|
// Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
|
|
529
553
|
// Null when no FlowDefinition is active (fallback: helpers use workflow.* prefix only).
|
|
530
|
-
const declaredClaimTypes =
|
|
531
|
-
? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
|
|
532
|
-
: null;
|
|
554
|
+
const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
|
|
533
555
|
const warnings = [];
|
|
534
556
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
535
557
|
const base = relative(root, artifactDir);
|
|
@@ -890,10 +912,39 @@ function verifyCommandLogChain(artifactDir) {
|
|
|
890
912
|
* FLOW_AGENTS_GOAL_FIT_RECHECK=true (the RCE-risky opt-in path).
|
|
891
913
|
* Returns { argv, cwd, source } or null when nothing trusted resolves.
|
|
892
914
|
*/
|
|
915
|
+
// WS8 (AC10b): a kind:"command" evidence ref's excerpt/command must be a literally runnable
|
|
916
|
+
// shell command, not a prose description of a manual verification step. This heuristic
|
|
917
|
+
// rejects prose so the goal-fit backstop never spawns `bash -lc "<a sentence>"` and
|
|
918
|
+
// misreports the resulting shell error as a caught false-completion. Pairs with the rule
|
|
919
|
+
// stated in context/contracts/planning-contract.md (AC11).
|
|
920
|
+
function isRunnableCommandText(text) {
|
|
921
|
+
const s = typeof text === 'string' ? text.trim() : '';
|
|
922
|
+
if (!s) return false;
|
|
923
|
+
// Sentence-like prose (a . ! or ? followed by more words) is not a single command.
|
|
924
|
+
if (/[.!?]\s+\S/.test(s)) return false;
|
|
925
|
+
const first = s.split(/\s+/)[0] || '';
|
|
926
|
+
// WS8 (AC10b, iteration 2): the first-token allowlist is broadened to the common
|
|
927
|
+
// real-world verify binaries a project's runnable evidence names (git, tsc, eslint,
|
|
928
|
+
// python/python3, docker, curl, jq, diff, grep, test) so honest command evidence is not
|
|
929
|
+
// misclassified as prose. Still fail-closed for genuinely non-command prose: a bare
|
|
930
|
+
// sentence whose first token is none of these (and carries no path prefix or shell
|
|
931
|
+
// metacharacter) is rejected rather than executed.
|
|
932
|
+
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)
|
|
933
|
+
|| first.startsWith('./') || first.startsWith('/') || first.includes('/');
|
|
934
|
+
const hasShellMeta = /[|&;<>()$`]/.test(s);
|
|
935
|
+
return knownBinary || hasShellMeta;
|
|
936
|
+
}
|
|
937
|
+
|
|
893
938
|
function resolveTrustedCommand(root, artifactDir, check, acceptance) {
|
|
894
939
|
// (a) acceptance criterion command for the matching criterion.
|
|
895
940
|
const fromAcceptance = acceptanceCommandFor(check, acceptance);
|
|
896
|
-
if (fromAcceptance)
|
|
941
|
+
if (fromAcceptance) {
|
|
942
|
+
// WS8 (AC10b): never spawn a prose "excerpt" as bash. A kind:"command" ref whose text
|
|
943
|
+
// is not a runnable shell command is malformed-evidence — reported distinctly, not
|
|
944
|
+
// executed, and not conflated with a caught false-completion.
|
|
945
|
+
if (!isRunnableCommandText(fromAcceptance)) return { malformed: fromAcceptance };
|
|
946
|
+
return { argv: ['bash', '-lc', fromAcceptance], cwd: root, source: 'acceptance' };
|
|
947
|
+
}
|
|
897
948
|
|
|
898
949
|
// (b) declared manifest target. Map the check command/id to a declared script.
|
|
899
950
|
const declared = declaredManifestTarget(root, check);
|
|
@@ -1020,9 +1071,7 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1020
1071
|
// Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
|
|
1021
1072
|
// Null when no FlowDefinition is active (fallback: bundleClaimedPassCommandChecks
|
|
1022
1073
|
// uses workflow.check.* prefix only — no regression for non-FlowDefinition sessions).
|
|
1023
|
-
const declaredClaimTypes =
|
|
1024
|
-
? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
|
|
1025
|
-
: null;
|
|
1074
|
+
const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
|
|
1026
1075
|
const bundle = readJsonFile(path.join(artifactDir, 'trust.bundle'));
|
|
1027
1076
|
const acceptance = readJsonFile(path.join(artifactDir, 'acceptance.json'));
|
|
1028
1077
|
const log = readLatestCommandLog(artifactDir); // Fix C: latest-wins; genuine fix-then-rerun-to-pass clears the block
|
|
@@ -1157,6 +1206,12 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
|
|
|
1157
1206
|
continue;
|
|
1158
1207
|
}
|
|
1159
1208
|
const trusted = resolveTrustedCommand(root, artifactDir, check, acceptance);
|
|
1209
|
+
if (trusted && trusted.malformed) {
|
|
1210
|
+
// WS8 (AC10b): the matching acceptance criterion named a kind:"command" evidence ref
|
|
1211
|
+
// whose text is prose, not a runnable command. Do NOT execute it; classify it.
|
|
1212
|
+
warnings.push(`${base} evidence check ${id}: malformed-evidence — acceptance criterion names a kind:"command" evidence ref whose text is not a runnable shell command ("${safeOneLine(trusted.malformed, 120)}"); it was NOT executed. Use a literal runnable command, or record the check as not_verified/accepted_gap (see context/contracts/planning-contract.md).`);
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1160
1215
|
if (!trusted) {
|
|
1161
1216
|
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.`);
|
|
1162
1217
|
continue;
|
|
@@ -1360,9 +1415,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
|
|
|
1360
1415
|
// When activeFlowStep is non-null, select claims whose claimType is in the
|
|
1361
1416
|
// gate's declared set. When null, fall back to the existing workflow.* prefix
|
|
1362
1417
|
// filter so no-FlowDefinition sessions are unaffected.
|
|
1363
|
-
const declaredClaimTypes =
|
|
1364
|
-
? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
|
|
1365
|
-
: null;
|
|
1418
|
+
const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
|
|
1366
1419
|
|
|
1367
1420
|
// SECURITY (Layer 2 — gate-bypass-chain fix): use UNION form instead of if/else.
|
|
1368
1421
|
// With the old if/else, an empty declaredClaimTypes (Set{}) from a fake flow with
|
|
@@ -1473,9 +1526,9 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
|
|
|
1473
1526
|
}
|
|
1474
1527
|
|
|
1475
1528
|
/**
|
|
1476
|
-
* Scope to the session's current task when .flow-agents/current.json points at
|
|
1529
|
+
* Scope to the session's current task when .kontourai/flow-agents/current.json points at
|
|
1477
1530
|
* one (mirroring evidence-capture.js). Returns the slug dir, or null to fall back
|
|
1478
|
-
* to scanning all of .flow-agents (newest-mtime).
|
|
1531
|
+
* to scanning all of .kontourai/flow-agents (newest-mtime).
|
|
1479
1532
|
*/
|
|
1480
1533
|
function preferredArtifactDir(flowAgentsDir) {
|
|
1481
1534
|
const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
|
|
@@ -1487,6 +1540,24 @@ function preferredArtifactDir(flowAgentsDir) {
|
|
|
1487
1540
|
return dir.startsWith(flowAgentsDir + path.sep) && fs.existsSync(dir) ? dir : null;
|
|
1488
1541
|
}
|
|
1489
1542
|
|
|
1543
|
+
/** WS8 (AC10a): a session dir is eligible as "active" only with real sidecar presence. */
|
|
1544
|
+
function hasSidecarPresence(artifactDir) {
|
|
1545
|
+
return fs.existsSync(path.join(artifactDir, 'state.json')) || fs.existsSync(path.join(artifactDir, 'trust.bundle'));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// WS8 (AC10a): when current.json names a slug whose session directory does NOT exist,
|
|
1549
|
+
// return that slug so analyze() can log the staleness rather than silently falling back to
|
|
1550
|
+
// a global mtime scan that could resurface an abandoned/never-real session as active.
|
|
1551
|
+
function staleCurrentSlug(flowAgentsDir) {
|
|
1552
|
+
const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
|
|
1553
|
+
if (!current) return null;
|
|
1554
|
+
const slug = current.artifact_dir || current.active_slug;
|
|
1555
|
+
if (typeof slug !== 'string' || !slug.trim()) return null;
|
|
1556
|
+
const safe = slug.replace(/\.\.+/g, '').replace(/^[/\\]+/, '');
|
|
1557
|
+
const dir = path.join(flowAgentsDir, safe);
|
|
1558
|
+
return (dir.startsWith(flowAgentsDir + path.sep) && !fs.existsSync(dir)) ? slug : null;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1490
1561
|
/**
|
|
1491
1562
|
* A task is pre-execution (work not yet started) when its state.json status/phase
|
|
1492
1563
|
* is still in the idea→planning band, or (no state.json) its markdown status is.
|
|
@@ -1516,9 +1587,7 @@ function isPreExecution(artifactDir, markdownStatus) {
|
|
|
1516
1587
|
// ADR 0016 P-c: pass activeFlowStep so bundlePendingCriteriaCount includes declared types.
|
|
1517
1588
|
function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
|
|
1518
1589
|
// Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
|
|
1519
|
-
const declaredClaimTypes =
|
|
1520
|
-
? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
|
|
1521
|
-
: null;
|
|
1590
|
+
const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
|
|
1522
1591
|
const warnings = [];
|
|
1523
1592
|
const hasBundle = fs.existsSync(path.join(artifactDir, 'trust.bundle'));
|
|
1524
1593
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
@@ -1576,19 +1645,29 @@ function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
|
|
|
1576
1645
|
// MAX_BLOCKS hard-block guard (preventing auto-release of hard blocks).
|
|
1577
1646
|
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/;
|
|
1578
1647
|
// FULL_BLOCK adds: workflow-state hygiene, surface-unavailable fail-closed, missing log.
|
|
1579
|
-
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/;
|
|
1648
|
+
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/;
|
|
1580
1649
|
|
|
1581
1650
|
async function analyze(root, now = Date.now()) {
|
|
1582
1651
|
const flowAgentsDirs = flowAgentsArtifactRootsForRead(root);
|
|
1583
1652
|
// Scope to the session's current task when current.json names one, so an
|
|
1584
1653
|
// unrelated active workflow elsewhere in the repo does not gate this stop.
|
|
1585
1654
|
const scoped = flowAgentsDirs.map(preferredArtifactDir).find(Boolean);
|
|
1655
|
+
// WS8 (AC10a): if current.json points at a nonexistent slug, LOG the staleness and, in
|
|
1656
|
+
// the global fallback, require sidecar presence so a stale pointer cannot resurface an
|
|
1657
|
+
// abandoned/never-real markdown-only directory as "the active session".
|
|
1658
|
+
const staleSlug = scoped ? null : flowAgentsDirs.map(staleCurrentSlug).find(Boolean);
|
|
1659
|
+
if (staleSlug) {
|
|
1660
|
+
process.stderr.write(`[Hook] Goal Fit: current.json names slug "${safeOneLine(staleSlug, 80)}" but no such session directory exists — ignoring the stale pointer instead of resurfacing an abandoned session via a global mtime scan.\n`);
|
|
1661
|
+
}
|
|
1586
1662
|
const searchDirs = scoped ? [scoped] : flowAgentsDirs;
|
|
1587
|
-
|
|
1663
|
+
let artifacts = searchDirs
|
|
1588
1664
|
.flatMap(dir => walkMarkdown(dir))
|
|
1589
1665
|
.map(readArtifact)
|
|
1590
1666
|
.filter(isWorkflowArtifact)
|
|
1591
1667
|
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1668
|
+
if (staleSlug) {
|
|
1669
|
+
artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
|
|
1670
|
+
}
|
|
1592
1671
|
|
|
1593
1672
|
if (artifacts.length === 0) return { warnings: [], blocking: false };
|
|
1594
1673
|
|
|
@@ -1664,7 +1743,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1664
1743
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
1665
1744
|
return blockRe.test(w);
|
|
1666
1745
|
});
|
|
1667
|
-
return { warnings, blocking, preExecution };
|
|
1746
|
+
return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep) };
|
|
1668
1747
|
}
|
|
1669
1748
|
|
|
1670
1749
|
/**
|
|
@@ -1717,6 +1796,52 @@ function bumpBlockStreak(root, hash) {
|
|
|
1717
1796
|
return count;
|
|
1718
1797
|
}
|
|
1719
1798
|
|
|
1799
|
+
/**
|
|
1800
|
+
* Message-assembly only: per-gap remediation guidance (linter-style " → " line)
|
|
1801
|
+
* appended under a matching warning. Fix-first / accept-second ordering per
|
|
1802
|
+
* class; the "accept" path always names a reason requirement and is never the
|
|
1803
|
+
* only or primary option. Does not affect which warnings are emitted, HARD_BLOCK/
|
|
1804
|
+
* FULL_BLOCK classification, block counting, or exit codes — text only.
|
|
1805
|
+
* Returns null when a warning does not match one of the four known classes.
|
|
1806
|
+
*/
|
|
1807
|
+
function remediationFor(warning) {
|
|
1808
|
+
const w = String(warning || '');
|
|
1809
|
+
|
|
1810
|
+
// (1) claimed pass but the trusted re-run failed — reuse the exact command
|
|
1811
|
+
// already named in the warning text; never invent a different one.
|
|
1812
|
+
// NOTE: anchor on the quoted command substring + fixed tail, NOT on
|
|
1813
|
+
// `trusted backstop (<source>)` paren-matching. trusted.source can itself
|
|
1814
|
+
// contain parens (e.g. "model-command (FLOW_AGENTS_GOAL_FIT_RECHECK)"), and a
|
|
1815
|
+
// non-nesting `[^)]*` silently fails to match that nested-paren source,
|
|
1816
|
+
// dropping the guidance line for the RECHECK backstop path.
|
|
1817
|
+
const rerun = /re-run of "([\s\S]+?)" FAILED with exit \d+, contradicting the claimed pass/.exec(w);
|
|
1818
|
+
if (rerun) {
|
|
1819
|
+
return ` → run: ${rerun[1]} — fix the failures, then re-record the check`;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// (2) claimed pass never captured (no trusted command resolved, backstop
|
|
1823
|
+
// disabled via FLOW_AGENTS_GOAL_FIT_BACKSTOP=skip, or the backstop could not launch).
|
|
1824
|
+
// Same nested-paren fix as (1): match on the invariant "could not run" tail
|
|
1825
|
+
// rather than paren-counting the `(<source>)` segment.
|
|
1826
|
+
if (/claimed pass but NOT_VERIFIED — command ".+?" was never captured/.test(w)
|
|
1827
|
+
|| /claimed pass but NOT_VERIFIED — trusted backstop .*could not run/.test(w)) {
|
|
1828
|
+
return " → run the command in this session (evidence capture is active), then re-record the check with the exact command string via: npm run workflow:sidecar -- record-evidence <artifact-dir> --verdict <v> --check-json '...'";
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// (3) critique verdict fail / open findings.
|
|
1832
|
+
if (/critique status:/.test(w) || /critique open /.test(w)) {
|
|
1833
|
+
return ' → resolve the findings and record a passing critique (record-critique), or mark each finding accepted with a reason';
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// (4) evidence verdict fail / missing acceptance evidence.
|
|
1837
|
+
if (/evidence verdict:/.test(w) || /evidence check .+ status:/.test(w)
|
|
1838
|
+
|| /evidence NOT_VERIFIED gap:/.test(w) || /Final Acceptance:/.test(w)) {
|
|
1839
|
+
return ' → fix the failing check, or record it as an accepted gap with justification in the session artifact';
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1720
1845
|
async function run(rawInput) {
|
|
1721
1846
|
const input = parseJson(rawInput);
|
|
1722
1847
|
const root = findRepoRoot(input.cwd || process.cwd());
|
|
@@ -1728,9 +1853,15 @@ async function run(rawInput) {
|
|
|
1728
1853
|
return rawInput;
|
|
1729
1854
|
}
|
|
1730
1855
|
|
|
1856
|
+
const gatePrefix = result.gatePrefix || '[stop-gate]';
|
|
1731
1857
|
const message = [
|
|
1732
|
-
|
|
1733
|
-
...result.warnings.
|
|
1858
|
+
`${gatePrefix} Goal Fit warning:`,
|
|
1859
|
+
...result.warnings.flatMap(w => {
|
|
1860
|
+
const lines = [` - ${w}`];
|
|
1861
|
+
const guidance = remediationFor(w);
|
|
1862
|
+
if (guidance) lines.push(guidance);
|
|
1863
|
+
return lines;
|
|
1864
|
+
}),
|
|
1734
1865
|
].join('\n');
|
|
1735
1866
|
|
|
1736
1867
|
if (mode !== 'block' || !result.blocking) {
|
|
@@ -1746,7 +1877,7 @@ async function run(rawInput) {
|
|
|
1746
1877
|
// N identical hard blocks to escape via the streak counter must not work.
|
|
1747
1878
|
//
|
|
1748
1879
|
// BAR-RAISER, NOT AIRTIGHT: an agent with shell access can still reset the streak
|
|
1749
|
-
//
|
|
1880
|
+
// with runtime-constructed paths or by modifying the warning
|
|
1750
1881
|
// text so the hash changes. The real anchor is external (signed checkpoints + human
|
|
1751
1882
|
// review). This raises the cost of the burn-through-the-counter escape vector.
|
|
1752
1883
|
const isHardBlock = result.warnings.some(w => {
|
|
@@ -1757,20 +1888,20 @@ async function run(rawInput) {
|
|
|
1757
1888
|
// Do NOT clear the streak — keep accumulating so the same hard block stays visible.
|
|
1758
1889
|
return {
|
|
1759
1890
|
stdout: rawInput,
|
|
1760
|
-
stderr: `${message}\n
|
|
1891
|
+
stderr: `${message}\n${gatePrefix} max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
|
|
1761
1892
|
exitCode: 2,
|
|
1762
1893
|
};
|
|
1763
1894
|
}
|
|
1764
1895
|
clearBlockStreak(root);
|
|
1765
1896
|
return {
|
|
1766
1897
|
stdout: rawInput,
|
|
1767
|
-
stderr: `${message}\n
|
|
1898
|
+
stderr: `${message}\n${gatePrefix} released — the same gap(s) blocked ${count}x without progress; needs your decision (listed above).`,
|
|
1768
1899
|
exitCode: 0,
|
|
1769
1900
|
};
|
|
1770
1901
|
}
|
|
1771
1902
|
return {
|
|
1772
1903
|
stdout: rawInput,
|
|
1773
|
-
stderr: `${message}\n
|
|
1904
|
+
stderr: `${message}\n${gatePrefix} Stop blocked — ${result.warnings.length} evidence gap(s) (block ${count}; after ${maxBlocks} identical blocks I stop blocking and hand this to you)`,
|
|
1774
1905
|
exitCode: 2,
|
|
1775
1906
|
};
|
|
1776
1907
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
|
|
20
|
+
const { resolveActor } = require('./lib/actor-identity');
|
|
20
21
|
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
21
22
|
|
|
22
23
|
const STEERING = {
|
|
@@ -205,6 +206,52 @@ function contextMapSteering(root) {
|
|
|
205
206
|
].join(' ');
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
function promptText(input) {
|
|
210
|
+
const candidates = [
|
|
211
|
+
input && input.prompt,
|
|
212
|
+
input && input.user_prompt,
|
|
213
|
+
input && input.message,
|
|
214
|
+
input && input.text,
|
|
215
|
+
input && input.tool_input && input.tool_input.prompt,
|
|
216
|
+
];
|
|
217
|
+
for (const candidate of candidates) {
|
|
218
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate;
|
|
219
|
+
}
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function looksLikeBuilderWork(text) {
|
|
224
|
+
const normalized = safeStateText(text, 2000).toLowerCase();
|
|
225
|
+
if (!normalized) return false;
|
|
226
|
+
const readOnlyIntent = /\b(read[- ]only|review[- ]only|no (source |code |file )?(changes?|edits?|modifications?)|do not (modify|edit|change|write|fix|implement|touch)|don't (modify|edit|change|write|fix|implement|touch)|dont (modify|edit|change|write|fix|implement|touch))\b/.test(normalized);
|
|
227
|
+
if (readOnlyIntent) return false;
|
|
228
|
+
const questionOnlyIntent = /^(what|which|who|when|where|why|how)\b/.test(normalized);
|
|
229
|
+
if (questionOnlyIntent) return false;
|
|
230
|
+
const noCodeQuestion = /\b(explain|describe|summari[sz]e|what is|how does|how do i|why does|review|critique)\b/.test(normalized)
|
|
231
|
+
&& !/\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
|
|
232
|
+
if (noCodeQuestion) return false;
|
|
233
|
+
const verificationOnlyIntent = /\b(validate|test|verify|check)\b/.test(normalized)
|
|
234
|
+
&& !/\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
|
|
235
|
+
if (verificationOnlyIntent) return false;
|
|
236
|
+
const hasWorkVerb = /\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
|
|
237
|
+
const hasWorkObject = /\b(api|endpoint|component|feature|bug|failing test|regression|integration|hook|script|cli|config|schema|migration)\b/.test(normalized);
|
|
238
|
+
const hasRequestLanguage = /\b(can you|please|let's|lets|need|should|make|ensure|support|add|update|fix|work on)\b/.test(normalized);
|
|
239
|
+
return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function builderWorkflowSteering(input) {
|
|
243
|
+
if (!looksLikeBuilderWork(promptText(input))) return '';
|
|
244
|
+
return [
|
|
245
|
+
'BUILDER WORKFLOW ROUTE: this user prompt looks like coding/build work.',
|
|
246
|
+
'Builder lifecycle: shape raw ideas -> build selected work -> publish verified branch/PR -> learn correction feedback.',
|
|
247
|
+
'Before source edits or implementation commands, activate Builder Kit delivery workflow.',
|
|
248
|
+
'If the user explicitly requested TDD, activate `tdd-workflow`; otherwise activate `deliver` and keep the session on `builder.build`.',
|
|
249
|
+
'Use `npm run workflow:sidecar -- ensure-session --flow-id builder.build ...` when the repo provides the sidecar writer.',
|
|
250
|
+
'After local verification, continue to publish/release-readiness and learning-review; do not treat local verification as terminal delivery.',
|
|
251
|
+
'Do not bypass plan-work -> execute-plan -> review-work -> verify-work for coding tasks; direct implementation is only acceptable when the user explicitly asks for no workflow or explanation-only help.',
|
|
252
|
+
].join(' ');
|
|
253
|
+
}
|
|
254
|
+
|
|
208
255
|
/**
|
|
209
256
|
* Compose the RESUME block for SessionStart.
|
|
210
257
|
*
|
|
@@ -297,15 +344,20 @@ function resumeSteering(root, current) {
|
|
|
297
344
|
|
|
298
345
|
// Liveness advisory
|
|
299
346
|
try {
|
|
347
|
+
const resolved = resolveActor(process.env);
|
|
348
|
+
const selfActor = resolved.actor || 'local';
|
|
300
349
|
const events = flowAgentsArtifactRootsForRead(root)
|
|
301
350
|
.flatMap(artifactRoot => readLivenessEvents(path.join(artifactRoot, 'liveness', 'events.jsonl')));
|
|
302
351
|
if (events.length > 0) {
|
|
303
|
-
|
|
352
|
+
// selfActor (for exclusion matching) stays raw here; only the display strings pushed
|
|
353
|
+
// into `lines` are sanitized below, since holders come from the shared multi-writer
|
|
354
|
+
// liveness stream and must be treated as untrusted display input (#287 fix iteration 1).
|
|
304
355
|
const holders = freshHolders(events, slug, selfActor, Date.now());
|
|
305
356
|
for (const h of holders) {
|
|
306
|
-
lines.push(`[LIVENESS WARNING: another agent appears live on this work: actor ${h.actor}, last seen ${h.lastAt}]`);
|
|
357
|
+
lines.push(`[LIVENESS WARNING: another agent appears live on this work: actor ${safeStateText(h.actor)}, last seen ${h.lastAt}]`);
|
|
307
358
|
}
|
|
308
359
|
}
|
|
360
|
+
lines.push(`ACTOR: ${safeStateText(selfActor)} (${resolved.source})`);
|
|
309
361
|
} catch { /* skip */ }
|
|
310
362
|
|
|
311
363
|
// Pull-work route hint
|
|
@@ -348,6 +400,15 @@ function run(rawInput) {
|
|
|
348
400
|
}
|
|
349
401
|
}
|
|
350
402
|
|
|
403
|
+
if (event === 'UserPromptSubmit') {
|
|
404
|
+
const builderHint = builderWorkflowSteering(input);
|
|
405
|
+
if (builderHint) {
|
|
406
|
+
hints.push(builderHint);
|
|
407
|
+
const contextHint = contextMapSteering(root);
|
|
408
|
+
if (contextHint) hints.push(contextHint);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
351
412
|
// SessionStart only: append the RESUME block for richer situational awareness
|
|
352
413
|
if (event === 'SessionStart' && current) {
|
|
353
414
|
const resumeBlock = resumeSteering(root, current);
|
|
@@ -379,4 +440,17 @@ if (require.main === module) {
|
|
|
379
440
|
});
|
|
380
441
|
}
|
|
381
442
|
|
|
382
|
-
module.exports = {
|
|
443
|
+
module.exports = {
|
|
444
|
+
run,
|
|
445
|
+
stateSteering,
|
|
446
|
+
critiqueSteering,
|
|
447
|
+
contextMapSteering,
|
|
448
|
+
latestWorkflowState,
|
|
449
|
+
findRepoRoot,
|
|
450
|
+
safeStateText,
|
|
451
|
+
stateNeedsAmbientSteering,
|
|
452
|
+
resumeSteering,
|
|
453
|
+
promptText,
|
|
454
|
+
looksLikeBuilderWork,
|
|
455
|
+
builderWorkflowSteering,
|
|
456
|
+
};
|