@kontourai/flow-agents 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +32 -0
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +39 -0
- package/CONTEXT.md +14 -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/console-learning-projection.js +3 -2
- 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 +22 -6
- 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 +792 -69
- 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 +23 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +38 -7
- package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
- package/context/contracts/decision-registry-contract.md +263 -0
- package/context/contracts/delivery-contract.md +1 -1
- package/context/contracts/planning-contract.md +2 -1
- package/context/contracts/review-contract.md +5 -3
- package/context/contracts/verification-contract.md +9 -1
- package/context/contracts/work-item-contract.md +1 -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/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 +283 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +9 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +14 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +3 -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 +1 -1
- package/docs/workflow-usage-guide.md +254 -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 +65 -1
- 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_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_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 +2 -2
- package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +4 -4
- 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 +2 -2
- 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 +151 -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 +723 -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 +22 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +56 -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 +13 -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 +80 -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/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 +8 -4
- 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/decision-record.schema.json +109 -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 +436 -79
- 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/console-learning-projection.ts +3 -2
- 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 +22 -6
- 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 +795 -71
- 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 +23 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* actor-identity.js — shared pure-CJS runtime-agnostic actor identity resolver
|
|
4
|
+
*
|
|
5
|
+
* Zero external dependencies (only Node core: fs, os, crypto, child_process).
|
|
6
|
+
* Consumed by:
|
|
7
|
+
* - scripts/hooks/workflow-steering.js (CJS, direct require)
|
|
8
|
+
* - build/src/cli/workflow-sidecar.js (ESM compiled, via createRequire)
|
|
9
|
+
*
|
|
10
|
+
* Purpose (issue #287): retire the shared literal `"local"` liveness-actor
|
|
11
|
+
* default. A fresh Bash-tool subshell cannot inherit env exported by a
|
|
12
|
+
* sibling hook subprocess, so this module recomputes the same actor on
|
|
13
|
+
* every invocation from a priority chain, rather than relying on any
|
|
14
|
+
* hook-writes-env or last-writer-wins persisted file:
|
|
15
|
+
*
|
|
16
|
+
* 1. `FLOW_AGENTS_ACTOR` env override, explicit, always wins (unless it is
|
|
17
|
+
* the literal `"local"`, case-insensitive, or it strips to empty under
|
|
18
|
+
* the allowed `[A-Za-z0-9_.-]` charset — neither value can round trip
|
|
19
|
+
* back in via the override seam; either rejection emits one stderr
|
|
20
|
+
* warning line, never silently substituting the shared `"unknown"`
|
|
21
|
+
* sentinel). A deliberate literal `"unknown"` override still passes
|
|
22
|
+
* through (it sanitizes to itself). The accepted override value is
|
|
23
|
+
* passed through `sanitizeSegment` (64-char cap; strips `:`) before use
|
|
24
|
+
* — it is never returned verbatim.
|
|
25
|
+
* 2. A runtime-native session-id env var already ambient in the current
|
|
26
|
+
* process's own environment (confirmed for Claude Code:
|
|
27
|
+
* `CLAUDECODE`/`CLAUDE_CODE_SESSION_ID`; Codex/opencode/pi candidate
|
|
28
|
+
* var names below are UNVERIFIED in this planning pass — accepted gap,
|
|
29
|
+
* see plan artifact — layer 3 is the correctness backstop for them).
|
|
30
|
+
* 3. A process-ancestry fallback: `process.ppid` plus that parent
|
|
31
|
+
* process's exact start timestamp (an absolute timestamp, not an
|
|
32
|
+
* elapsed-time subtraction, to avoid clock-drift flicker across
|
|
33
|
+
* repeated invocations seconds/minutes apart within one session),
|
|
34
|
+
* hashed into a short opaque token. Works identically for a Bash-tool
|
|
35
|
+
* shell or a hook subprocess, since both are children of the same
|
|
36
|
+
* long-lived runtime process for that session.
|
|
37
|
+
*
|
|
38
|
+
* Accepted gap: sandboxed/containerized environments without a working
|
|
39
|
+
* `ps`/`/proc` degrade the ancestry fallback to a PID-only seed (no
|
|
40
|
+
* start-time component), which is collision-prone under PID reuse on a
|
|
41
|
+
* long-uptime host. This is documented, not silently absorbed as "solved".
|
|
42
|
+
*
|
|
43
|
+
* Exports:
|
|
44
|
+
* detectRuntime(env) → "claude-code" | "codex" | "opencode" | "pi" | "unknown"
|
|
45
|
+
* runtimeSessionId(env) → first non-empty runtime-native session id, else ""
|
|
46
|
+
* ancestorActorSeed() → short opaque token from parent PID + start time, else ""
|
|
47
|
+
* sanitizeSegment(value) → value restricted to [A-Za-z0-9_.-], capped 64 chars
|
|
48
|
+
* serializeActor(actor) → actor struct serialized to a single grouping-key-safe string
|
|
49
|
+
* resolveActor(env) → { actor: string, source: string }
|
|
50
|
+
* isUnresolvedActor(actor) → boolean (true when actor is empty or the retired literal
|
|
51
|
+
* "local", case-insensitive — single-sourced predicate shared by
|
|
52
|
+
* the lifecycle auto-emit path, the direct CLI liveness path, and
|
|
53
|
+
* (#288) the tool-activity heartbeat path, so all three can never
|
|
54
|
+
* disagree on what counts as "no usable actor")
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
const os = require('os');
|
|
59
|
+
const crypto = require('crypto');
|
|
60
|
+
const { execFileSync } = require('child_process');
|
|
61
|
+
|
|
62
|
+
/** Candidate env var names for each runtime's native session id. */
|
|
63
|
+
const RUNTIME_SESSION_ID_VARS = {
|
|
64
|
+
'claude-code': 'CLAUDE_CODE_SESSION_ID',
|
|
65
|
+
// Codex/opencode/pi candidate names are UNVERIFIED (no confirmed spike this
|
|
66
|
+
// planning pass) — accepted gap. Detection failure never blocks resolution;
|
|
67
|
+
// the process-ancestry fallback (layer 3) covers these runtimes either way.
|
|
68
|
+
codex: 'CODEX_SESSION_ID',
|
|
69
|
+
opencode: 'OPENCODE_SESSION_ID',
|
|
70
|
+
pi: 'PI_SESSION_ID',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detect which coding-agent runtime the current process is running under.
|
|
75
|
+
* Detection failure must never block actor resolution — always falls back
|
|
76
|
+
* to "unknown" rather than throwing.
|
|
77
|
+
*
|
|
78
|
+
* @param {NodeJS.ProcessEnv} [env] Environment to inspect (default process.env)
|
|
79
|
+
* @returns {"claude-code"|"codex"|"opencode"|"pi"|"unknown"}
|
|
80
|
+
*/
|
|
81
|
+
function detectRuntime(env = process.env) {
|
|
82
|
+
env = env || {};
|
|
83
|
+
if (env.CLAUDECODE === '1' || String(env.CLAUDE_CODE_SESSION_ID || '').trim()) {
|
|
84
|
+
return 'claude-code';
|
|
85
|
+
}
|
|
86
|
+
if (String(env.CODEX_SESSION_ID || '').trim()) return 'codex';
|
|
87
|
+
if (String(env.OPENCODE_SESSION_ID || '').trim()) return 'opencode';
|
|
88
|
+
if (String(env.PI_SESSION_ID || '').trim()) return 'pi';
|
|
89
|
+
return 'unknown';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the first non-empty runtime-native session id candidate present in
|
|
94
|
+
* the given environment, checked in a fixed order across all known
|
|
95
|
+
* runtimes (not just the detected one, so a misdetected/ambiguous env still
|
|
96
|
+
* resolves a usable id).
|
|
97
|
+
*
|
|
98
|
+
* @param {NodeJS.ProcessEnv} [env] Environment to inspect (default process.env)
|
|
99
|
+
* @returns {string} Non-empty session id, or "" if none present
|
|
100
|
+
*/
|
|
101
|
+
function runtimeSessionId(env = process.env) {
|
|
102
|
+
env = env || {};
|
|
103
|
+
for (const varName of Object.values(RUNTIME_SESSION_ID_VARS)) {
|
|
104
|
+
const candidate = String(env[varName] || '').trim();
|
|
105
|
+
if (candidate) return candidate;
|
|
106
|
+
}
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read the parent process's absolute start timestamp on BSD/macOS via
|
|
112
|
+
* `ps -o lstart= -p <ppid>` (parsed with Date.parse — an absolute wall-clock
|
|
113
|
+
* timestamp, not an elapsed-time subtraction).
|
|
114
|
+
*
|
|
115
|
+
* @param {number} ppid
|
|
116
|
+
* @returns {string} Epoch-ms string, or "" if unobtainable
|
|
117
|
+
*/
|
|
118
|
+
function getBsdAncestorStartTimeMs(ppid) {
|
|
119
|
+
try {
|
|
120
|
+
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(ppid)], {
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
123
|
+
timeout: 5000,
|
|
124
|
+
});
|
|
125
|
+
const trimmed = String(out || '').trim();
|
|
126
|
+
if (!trimmed) return '';
|
|
127
|
+
const parsedMs = Date.parse(trimmed);
|
|
128
|
+
return Number.isNaN(parsedMs) ? '' : String(parsedMs);
|
|
129
|
+
} catch {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read the parent process's absolute start timestamp on Linux via
|
|
136
|
+
* `/proc/<ppid>/stat` field 22 (starttime, in clock ticks since boot)
|
|
137
|
+
* converted through `/proc/stat`'s `btime` (boot time, seconds since epoch)
|
|
138
|
+
* plus the clock-tick rate (via `getconf CLK_TCK`, falling back to the
|
|
139
|
+
* standard USER_HZ default of 100 if unavailable). This yields an absolute
|
|
140
|
+
* epoch timestamp, not an elapsed-time subtraction.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} ppid
|
|
143
|
+
* @returns {string} Epoch-ms string, or "" if unobtainable
|
|
144
|
+
*/
|
|
145
|
+
function getLinuxAncestorStartTimeMs(ppid) {
|
|
146
|
+
try {
|
|
147
|
+
const statRaw = fs.readFileSync(`/proc/${ppid}/stat`, 'utf8');
|
|
148
|
+
// The comm field (2nd field) is parenthesized and may itself contain
|
|
149
|
+
// spaces/parens, so split on the *last* ")" rather than whitespace.
|
|
150
|
+
const closeParen = statRaw.lastIndexOf(')');
|
|
151
|
+
if (closeParen === -1) return '';
|
|
152
|
+
const rest = statRaw.slice(closeParen + 2).trim().split(/\s+/);
|
|
153
|
+
// Fields after ")" start at field 3 (state); field 22 (starttime) is
|
|
154
|
+
// therefore index 22 - 3 = 19 in this zero-indexed remainder array.
|
|
155
|
+
const startTicks = Number(rest[19]);
|
|
156
|
+
if (!Number.isFinite(startTicks)) return '';
|
|
157
|
+
|
|
158
|
+
const procStat = fs.readFileSync('/proc/stat', 'utf8');
|
|
159
|
+
const btimeMatch = procStat.match(/^btime\s+(\d+)/m);
|
|
160
|
+
if (!btimeMatch) return '';
|
|
161
|
+
const btimeSeconds = Number(btimeMatch[1]);
|
|
162
|
+
if (!Number.isFinite(btimeSeconds)) return '';
|
|
163
|
+
|
|
164
|
+
let clockTicksPerSec = 100; // USER_HZ default; accepted approximation if getconf fails.
|
|
165
|
+
try {
|
|
166
|
+
const getconfOut = execFileSync('getconf', ['CLK_TCK'], {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
169
|
+
timeout: 5000,
|
|
170
|
+
}).trim();
|
|
171
|
+
const parsedTicks = Number(getconfOut);
|
|
172
|
+
if (Number.isFinite(parsedTicks) && parsedTicks > 0) clockTicksPerSec = parsedTicks;
|
|
173
|
+
} catch {
|
|
174
|
+
/* fall back to the USER_HZ default above */
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const startSeconds = btimeSeconds + startTicks / clockTicksPerSec;
|
|
178
|
+
return String(Math.round(startSeconds * 1000));
|
|
179
|
+
} catch {
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compute a runtime-agnostic process-ancestry seed: the current process's
|
|
186
|
+
* parent PID plus that parent's exact (absolute) start timestamp, hashed
|
|
187
|
+
* into a short opaque token. Stable across repeated invocations within the
|
|
188
|
+
* same session (same parent PID, same start time), distinct across
|
|
189
|
+
* concurrent sessions on one host (different parent process per session).
|
|
190
|
+
*
|
|
191
|
+
* Degrades to a PID-only seed (documented accepted gap — collision-prone
|
|
192
|
+
* under PID reuse on a long-uptime host) when the start timestamp cannot be
|
|
193
|
+
* obtained (no working `ps`/`/proc`, e.g. some sandboxes). Returns "" only
|
|
194
|
+
* in the near-impossible case that `process.ppid` itself is unavailable.
|
|
195
|
+
*
|
|
196
|
+
* @returns {string} Short opaque hex token, or ""
|
|
197
|
+
*/
|
|
198
|
+
function ancestorActorSeed() {
|
|
199
|
+
try {
|
|
200
|
+
const ppid = process.ppid;
|
|
201
|
+
if (!ppid || ppid <= 0) return '';
|
|
202
|
+
|
|
203
|
+
const startTimeMs =
|
|
204
|
+
process.platform === 'linux'
|
|
205
|
+
? getLinuxAncestorStartTimeMs(ppid)
|
|
206
|
+
: getBsdAncestorStartTimeMs(ppid);
|
|
207
|
+
|
|
208
|
+
const seedInput = startTimeMs ? `${ppid}:${startTimeMs}` : `pid-only:${ppid}`;
|
|
209
|
+
return crypto.createHash('sha1').update(seedInput).digest('hex').slice(0, 12);
|
|
210
|
+
} catch {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Restrict a value to a grouping-key-safe segment: strip everything outside
|
|
217
|
+
* `[A-Za-z0-9_.-]`, cap at 64 chars, and fall back to `"unknown"` if empty
|
|
218
|
+
* after stripping. Guarantees the result never contains `:` (so it can
|
|
219
|
+
* never collide with the `::` grouping delimiter used elsewhere) and is
|
|
220
|
+
* never an empty string (so joined segments never produce doubled `:`).
|
|
221
|
+
*
|
|
222
|
+
* @param {*} value
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
function sanitizeSegment(value) {
|
|
226
|
+
const cleaned = String(value == null ? '' : value)
|
|
227
|
+
.replace(/[^A-Za-z0-9_.-]/g, '')
|
|
228
|
+
.slice(0, 64);
|
|
229
|
+
return cleaned || 'unknown';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Serialize a runtime-agnostic actor struct into a single string safe for
|
|
234
|
+
* the existing `${subjectId}::${actor}` grouping key: each field is passed
|
|
235
|
+
* through sanitizeSegment (so no raw `:` can appear inside any segment),
|
|
236
|
+
* then joined with a single `:` delimiter (never doubled).
|
|
237
|
+
*
|
|
238
|
+
* @param {{runtime?: string, session_id?: string, host?: string, human?: string}} actor
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function serializeActor(actor) {
|
|
242
|
+
actor = actor || {};
|
|
243
|
+
const parts = [
|
|
244
|
+
sanitizeSegment(actor.runtime),
|
|
245
|
+
sanitizeSegment(actor.session_id),
|
|
246
|
+
sanitizeSegment(actor.host),
|
|
247
|
+
];
|
|
248
|
+
if (actor.human != null && String(actor.human).trim() !== '') {
|
|
249
|
+
parts.push(sanitizeSegment(actor.human));
|
|
250
|
+
}
|
|
251
|
+
return parts.join(':');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the current process's actor identity via the priority chain:
|
|
256
|
+
* 1. `env.FLOW_AGENTS_ACTOR` (trimmed, non-empty, not literal "local"
|
|
257
|
+
* case-insensitive, and not stripping to empty under the allowed
|
|
258
|
+
* `[A-Za-z0-9_.-]` charset) — wins outright, but is passed through
|
|
259
|
+
* `sanitizeSegment` before being returned (64-char cap; strips `:`) so
|
|
260
|
+
* it is always grouping-key-safe and display-safe. Callers must NOT
|
|
261
|
+
* assume this value is returned verbatim — it never is. A deliberate
|
|
262
|
+
* literal `"unknown"` override is honored as-is (it sanitizes to
|
|
263
|
+
* itself) — only a value that strips to empty is rejected.
|
|
264
|
+
* 2. Runtime-native session id (via runtimeSessionId), serialized with
|
|
265
|
+
* detectRuntime() and os.hostname().
|
|
266
|
+
* 3. Process-ancestry fallback (via ancestorActorSeed), serialized the
|
|
267
|
+
* same way.
|
|
268
|
+
*
|
|
269
|
+
* `actor` is `""` only if every layer failed (near-impossible in practice,
|
|
270
|
+
* since layer 3 has no external dependency).
|
|
271
|
+
*
|
|
272
|
+
* When `FLOW_AGENTS_ACTOR` is set but rejected — either because it is the
|
|
273
|
+
* literal `"local"` (case-insensitive) or because it strips to empty under
|
|
274
|
+
* the allowed charset (e.g. `":::"`) — a single diagnostic line is written
|
|
275
|
+
* to `process.stderr` (never stdout, never thrown) so the substitution is
|
|
276
|
+
* never silent, and resolution falls through to the runtime/ancestry
|
|
277
|
+
* derivation below rather than adopting the shared `"unknown"` sentinel:
|
|
278
|
+
* - literal "local": `[actor-identity] ignoring FLOW_AGENTS_ACTOR=local
|
|
279
|
+
* (reserved legacy value); using derived actor`.
|
|
280
|
+
* - strips to empty: `[actor-identity] ignoring FLOW_AGENTS_ACTOR override
|
|
281
|
+
* (strips to empty under allowed charset [A-Za-z0-9_.-]); using derived
|
|
282
|
+
* actor`.
|
|
283
|
+
*
|
|
284
|
+
* Test-only escape hatch: requires BOTH
|
|
285
|
+
* `env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED === "1"` AND
|
|
286
|
+
* `env.NODE_ENV === "test"` — short-circuits to `{ actor: "", source:
|
|
287
|
+
* "test-forced-unresolved" }` before any real detection runs. This lets
|
|
288
|
+
* tests prove the fail-loud path deterministically without sabotaging
|
|
289
|
+
* `ps`/`/proc`, while the `NODE_ENV === "test"` requirement prevents the
|
|
290
|
+
* hatch from being tripped by an accidental/malicious env var outside a
|
|
291
|
+
* test harness.
|
|
292
|
+
*
|
|
293
|
+
* @param {NodeJS.ProcessEnv} [env] Environment to resolve from (default process.env)
|
|
294
|
+
* @returns {{actor: string, source: string}}
|
|
295
|
+
*/
|
|
296
|
+
function resolveActor(env = process.env) {
|
|
297
|
+
env = env || {};
|
|
298
|
+
|
|
299
|
+
if (env.FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED === '1' && env.NODE_ENV === 'test') {
|
|
300
|
+
return { actor: '', source: 'test-forced-unresolved' };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const explicit = String(env.FLOW_AGENTS_ACTOR || '').trim();
|
|
304
|
+
if (explicit) {
|
|
305
|
+
if (explicit.toLowerCase() === 'local') {
|
|
306
|
+
// Rejected literal "local" override: never silent — one stderr warning line, never stdout,
|
|
307
|
+
// never thrown (hooks parse stdout; this diagnostic must not break that contract).
|
|
308
|
+
try {
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
'[actor-identity] ignoring FLOW_AGENTS_ACTOR=local (reserved legacy value); using derived actor\n'
|
|
311
|
+
);
|
|
312
|
+
} catch { /* best-effort diagnostic only */ }
|
|
313
|
+
} else if (!/[A-Za-z0-9_.-]/.test(explicit)) {
|
|
314
|
+
// F7 (#287 fix iteration 2): the override strips to empty under sanitizeSegment's allowed
|
|
315
|
+
// charset — falling through to sanitizeSegment here would silently adopt its "unknown"
|
|
316
|
+
// fallback sentinel as if it were a deliberate value. Reject instead, with the same
|
|
317
|
+
// never-silent / never-stdout / never-thrown contract as the "local" rejection above. A
|
|
318
|
+
// literal "unknown" override is unaffected by this branch (it contains allowed chars, so it
|
|
319
|
+
// sanitizes to itself and returns via the branch below).
|
|
320
|
+
try {
|
|
321
|
+
process.stderr.write(
|
|
322
|
+
'[actor-identity] ignoring FLOW_AGENTS_ACTOR override (strips to empty under allowed charset [A-Za-z0-9_.-]); using derived actor\n'
|
|
323
|
+
);
|
|
324
|
+
} catch { /* best-effort diagnostic only */ }
|
|
325
|
+
} else {
|
|
326
|
+
return { actor: sanitizeSegment(explicit), source: 'explicit-override' };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const runtime = detectRuntime(env);
|
|
331
|
+
const sessionId = runtimeSessionId(env);
|
|
332
|
+
if (sessionId) {
|
|
333
|
+
const actor = serializeActor({ runtime, session_id: sessionId, host: os.hostname() });
|
|
334
|
+
return { actor, source: `runtime-session-id:${runtime}` };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const seed = ancestorActorSeed();
|
|
338
|
+
if (seed) {
|
|
339
|
+
const actor = serializeActor({ runtime, session_id: `anc-${seed}`, host: os.hostname() });
|
|
340
|
+
return { actor, source: 'process-ancestry' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { actor: '', source: 'unresolved' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* True when an actor is empty or the retired literal "local" (case-insensitive) — the one
|
|
348
|
+
* shared definition of "unresolved" (#287 fix iteration 1, F6; single-sourced here per #288's
|
|
349
|
+
* Wave 1 Task 1.1 so the lifecycle auto-emit path, the direct CLI liveness path, and the
|
|
350
|
+
* tool-activity heartbeat path all consume the same predicate rather than each forking their
|
|
351
|
+
* own copy).
|
|
352
|
+
*
|
|
353
|
+
* @param {string} actor
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
function isUnresolvedActor(actor) {
|
|
357
|
+
return !actor || String(actor).toLowerCase() === 'local';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
detectRuntime,
|
|
362
|
+
runtimeSessionId,
|
|
363
|
+
ancestorActorSeed,
|
|
364
|
+
sanitizeSegment,
|
|
365
|
+
serializeActor,
|
|
366
|
+
resolveActor,
|
|
367
|
+
isUnresolvedActor,
|
|
368
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// config-protection-remedies.js — the sanctioned-remedy table for
|
|
4
|
+
// scripts/hooks/config-protection.js.
|
|
5
|
+
//
|
|
6
|
+
// WS8 (AC15): extracted verbatim from config-protection.js so the remedy table has an
|
|
7
|
+
// independently testable, independently reviewable surface and the main hook file is
|
|
8
|
+
// smaller. Behavior-preserving: no message text or matching data changed.
|
|
9
|
+
//
|
|
10
|
+
// AC7 (config-protection): for sidecar/gate kill-switch paths the block message MUST name
|
|
11
|
+
// the sanctioned `npm run workflow:sidecar -- <command>` writer (or a human maintainer for
|
|
12
|
+
// host-owned files) and MUST NEVER advise disabling the config-protection hook.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const SHELL_PROFILE_REMEDY =
|
|
16
|
+
'There is no sanctioned automated writer for shell profiles; ask a human maintainer to edit it directly. Never disable this hook to make the write.';
|
|
17
|
+
|
|
18
|
+
const SANCTIONED_REMEDIES = {
|
|
19
|
+
'.claude/settings.json':
|
|
20
|
+
'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
|
|
21
|
+
'.claude/settings.local.json':
|
|
22
|
+
'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
|
|
23
|
+
'.kontourai/flow-agents/current.json':
|
|
24
|
+
'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
|
|
25
|
+
'.kontourai/flow-agents/.goal-fit-block-streak.json':
|
|
26
|
+
'This file is only mutated internally by the goal-fit Stop hook; there is no sanctioned agent writer. Never disable this hook to make the write.',
|
|
27
|
+
'.kontourai/flow-agents/<slug>/state.json':
|
|
28
|
+
'Use `npm run workflow:sidecar -- advance-state <artifact-dir> --status <status> --phase <phase> --summary ... --next-action ...`. Never disable this hook to make the write.',
|
|
29
|
+
'.kontourai/flow-agents/<slug>/trust.bundle':
|
|
30
|
+
'Use `npm run workflow:sidecar -- record-gate-claim` or `seal-checkpoint`, not a direct write. Never disable this hook to make the write.',
|
|
31
|
+
'delivery/trust.bundle':
|
|
32
|
+
'This is written automatically by the delivery publish step (`npm run workflow:sidecar -- publish-delivery` / `advance-state --status delivered`). Never disable this hook to make the write.',
|
|
33
|
+
'delivery/trust.checkpoint.json':
|
|
34
|
+
'This is written automatically by the delivery publish step (`npm run workflow:sidecar -- publish-delivery` / `advance-state --status delivered`). Never disable this hook to make the write.',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Sanctioned remedy for a protected path name (from checkProtectedPathPattern). */
|
|
38
|
+
function remedyFor(name) {
|
|
39
|
+
return SANCTIONED_REMEDIES[name] || SHELL_PROFILE_REMEDY;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ordered remedy lookup candidates for raw command-string (substring) matching.
|
|
44
|
+
* Each entry names a SANCTIONED_REMEDIES key and the literal needle(s) that,
|
|
45
|
+
* when found anywhere in the command text, identify that protected path.
|
|
46
|
+
*
|
|
47
|
+
* Needed because Pass 1 (in config-protection.js) tokenizes on whitespace and requires a
|
|
48
|
+
* token to END at the protected basename. Real interpreter-write commands embed the
|
|
49
|
+
* path inside a quoted string followed by punctuation (an inline interpreter
|
|
50
|
+
* eval flag calling fs.writeFileSync with a quoted path argument), so no
|
|
51
|
+
* token ends cleanly at the basename and Pass 1 never matches.
|
|
52
|
+
*
|
|
53
|
+
* Order matters: more specific paths (delivery/*) are listed before the
|
|
54
|
+
* generic slug-scoped basenames they would otherwise collide with (both
|
|
55
|
+
* the per-slug trust bundle and the delivery trust bundle share the
|
|
56
|
+
* basename 'trust.bundle') -- first match wins, deterministically.
|
|
57
|
+
*/
|
|
58
|
+
const REMEDY_COMMAND_CANDIDATES = [
|
|
59
|
+
{ name: 'delivery/trust.checkpoint.json', needles: ['delivery/trust.checkpoint.json', 'trust.checkpoint.json'] },
|
|
60
|
+
{ name: 'delivery/trust.bundle', needles: ['delivery/trust.bundle'] },
|
|
61
|
+
{ name: '.kontourai/flow-agents/<slug>/trust.bundle', needles: ['trust.bundle'] },
|
|
62
|
+
{ name: '.kontourai/flow-agents/<slug>/state.json', needles: ['state.json'] },
|
|
63
|
+
{ name: '.kontourai/flow-agents/.goal-fit-block-streak.json', needles: ['.goal-fit-block-streak.json'] },
|
|
64
|
+
{ name: '.kontourai/flow-agents/current.json', needles: ['current.json'] },
|
|
65
|
+
{ name: '.claude/settings.local.json', needles: ['settings.local.json'] },
|
|
66
|
+
{ name: '.claude/settings.json', needles: ['.claude/settings.json'] },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
module.exports = { SHELL_PROFILE_REMEDY, SANCTIONED_REMEDIES, remedyFor, REMEDY_COMMAND_CANDIDATES };
|