@kontourai/flow-agents 3.0.0 → 3.1.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/workflows/ci.yml +8 -0
- package/CHANGELOG.md +15 -0
- package/CONTEXT.md +67 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +143 -0
- package/build/src/cli/assignment-provider.js +106 -49
- package/build/src/cli/workflow-sidecar.d.ts +2 -2
- package/build/src/cli/workflow-sidecar.js +392 -35
- package/build/src/lib/flow-resolver.d.ts +12 -6
- package/build/src/lib/flow-resolver.js +30 -14
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/contracts/assignment-provider-contract.md +9 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/scripts/hooks/config-protection.js +14 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +4 -2
- package/context/scripts/hooks/workflow-steering.js +42 -0
- package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
- package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
- package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0006-typescript-first-source-policy.md +2 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0007-skill-audit.md +2 -0
- package/docs/adr/0008-kit-operation-boundary.md +2 -0
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
- package/docs/adr/0011-mcp-posture.md +2 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
- package/docs/adr/0013-context-lifecycle.md +2 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
- package/docs/adr/0016-three-hard-boundary-model.md +2 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
- package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
- package/docs/adr/0019-kit-dependency-ownership.md +2 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +2 -0
- package/docs/adr/README.md +49 -0
- package/docs/adr/index.md +34 -0
- package/docs/decisions/agent-coordination.md +20 -0
- package/docs/decisions/anti-gaming-trust-security.md +20 -0
- package/docs/decisions/context-lifecycle.md +18 -0
- package/docs/decisions/core-domain-kit-boundary.md +18 -0
- package/docs/decisions/flow-flow-agents-boundary.md +18 -0
- package/docs/decisions/flow-kit.md +20 -0
- package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
- package/docs/decisions/graph-knowledge-provider.md +63 -0
- package/docs/decisions/hook-core-kit-boundary.md +18 -0
- package/docs/decisions/index.md +19 -0
- package/docs/decisions/kit-dependency-ownership.md +18 -0
- package/docs/decisions/kit-operation-boundary.md +18 -0
- package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
- package/docs/decisions/kontour-resource-contract.md +18 -0
- package/docs/decisions/mcp-posture.md +18 -0
- package/docs/decisions/three-hard-boundary-model.md +18 -0
- package/docs/decisions/trust-reconcile.md +20 -0
- package/docs/decisions/typescript-source-policy.md +48 -0
- package/docs/decisions/workflow-enforcement.md +18 -0
- package/docs/decisions/workflow-trust-state.md +20 -0
- package/docs/fixture-ownership.md +1 -1
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/ci/run-baseline.sh +4 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/integration/test_current_json_per_actor.sh +516 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
- package/evals/run.sh +4 -0
- package/evals/static/test_knowledge_providers.sh +13 -4
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/deliver/SKILL.md +17 -0
- package/kits/builder/skills/design-probe/SKILL.md +37 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
- package/kits/knowledge/adapters/default-store/index.js +92 -4
- package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
- package/kits/knowledge/adapters/shared/codec.js +141 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +112 -4
- package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
- package/kits/knowledge/flows/promote.flow.json +84 -0
- package/kits/knowledge/kit.json +15 -0
- package/kits/knowledge/promote/distill.js +96 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
- package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
- package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
- package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
- package/kits/knowledge/promote/fixtures/session/state.json +19 -0
- package/kits/knowledge/promote/health.js +137 -0
- package/kits/knowledge/promote/index.js +176 -0
- package/kits/knowledge/promote/ingest.js +103 -0
- package/kits/knowledge/promote/lib.js +132 -0
- package/kits/knowledge/promote/link.js +84 -0
- package/kits/knowledge/promote/promote.test.js +174 -0
- package/kits/knowledge/providers/conformance/suite.test.js +18 -0
- package/kits/knowledge/providers/index.js +1 -0
- package/kits/knowledge/providers/neo4j/connection.js +121 -0
- package/kits/knowledge/providers/neo4j/cypher.js +190 -0
- package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
- package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
- package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
- package/kits/knowledge/providers/neo4j/index.js +280 -0
- package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
- package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
- package/kits/knowledge/providers/neo4j/sync.js +235 -0
- package/package.json +4 -2
- package/scripts/README.md +1 -0
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +14 -1
- package/scripts/hooks/evidence-capture.js +4 -1
- package/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/scripts/hooks/lib/current-pointer.js +123 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
- package/scripts/hooks/stop-goal-fit.js +4 -2
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +137 -55
- package/src/cli/workflow-sidecar.ts +430 -33
- package/src/lib/flow-resolver.ts +35 -14
- package/src/tools/validate-source-tree.ts +2 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -161,6 +161,14 @@ jobs:
|
|
|
161
161
|
continue-on-error: true
|
|
162
162
|
run: bash evals/ci/run-baseline.sh --check pull-work-assignment-join-integration
|
|
163
163
|
|
|
164
|
+
- name: Ensure-session ownership guard integration
|
|
165
|
+
continue-on-error: true
|
|
166
|
+
run: bash evals/ci/run-baseline.sh --check ensure-session-ownership-guard-integration
|
|
167
|
+
|
|
168
|
+
- name: Current.json per-actor integration
|
|
169
|
+
continue-on-error: true
|
|
170
|
+
run: bash evals/ci/run-baseline.sh --check current-json-per-actor-integration
|
|
171
|
+
|
|
164
172
|
- name: Finalize CI evidence
|
|
165
173
|
if: always()
|
|
166
174
|
run: bash evals/ci/run-baseline.sh --finalize
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.1.0](https://github.com/kontourai/flow-agents/compare/v3.0.0...v3.1.0) (2026-07-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **docs:** probe docs-write contract — vocabulary + decision deltas ([#311](https://github.com/kontourai/flow-agents/issues/311)) ([#371](https://github.com/kontourai/flow-agents/issues/371)) ([867c3b5](https://github.com/kontourai/flow-agents/commit/867c3b5b645c896e5df6886177171b05e73e8fa9))
|
|
9
|
+
* **knowledge:** neo4j knowledge-store provider — Cypher-backed [#317](https://github.com/kontourai/flow-agents/issues/317) interface (opt-in default) ([#373](https://github.com/kontourai/flow-agents/issues/373)) ([2ef2523](https://github.com/kontourai/flow-agents/commit/2ef2523cc65a5e56dec8dcbf84edc3718cfe190c))
|
|
10
|
+
* **knowledge:** promote sub-flow — ingest→distill→link→health (flow within a flow) ([#313](https://github.com/kontourai/flow-agents/issues/313)) ([#372](https://github.com/kontourai/flow-agents/issues/372)) ([b345d89](https://github.com/kontourai/flow-agents/commit/b345d89873c99f65f00d0fe8ede6a8702b1a2b43))
|
|
11
|
+
* **knowledge:** stable record identity — short-id prefix + slug aliases ([#339](https://github.com/kontourai/flow-agents/issues/339)) ([ff54bc7](https://github.com/kontourai/flow-agents/commit/ff54bc74f893321f13276cd147d500f91e1fc133))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
* **adr:** ADR freeze cutover + liveness coordination experiment ([#332](https://github.com/kontourai/flow-agents/issues/332)) ([#368](https://github.com/kontourai/flow-agents/issues/368)) ([2c845b8](https://github.com/kontourai/flow-agents/commit/2c845b81675db02622fee8fa1efa6aa197ce96c0))
|
|
17
|
+
|
|
3
18
|
## [3.0.0](https://github.com/kontourai/flow-agents/compare/v2.4.0...v3.0.0) (2026-07-04)
|
|
4
19
|
|
|
5
20
|
|
package/CONTEXT.md
CHANGED
|
@@ -66,6 +66,16 @@ _Avoid_: Graph database as the generic term (the model is provider-independent;
|
|
|
66
66
|
A configured implementation of the knowledge-store capability behind the Knowledge Graph model. Providers expose a read interface (nodes, edges, query-by-type) and a proposals-only write interface (proposeWrite returns a proposal, never mutating a human-curated store). The reference providers are markdown-vault (the Obsidian-shaped vault), git-repo (decision registry, CONTEXT.md vocabulary, learnings), and work-item (GitHub issues as a source/sink adapter). See context/contracts/knowledge-store-contract.md.
|
|
67
67
|
_Avoid_: Knowledge adapter when contrasting the provider interface with a single store adapter
|
|
68
68
|
|
|
69
|
+
### Graph Knowledge Provider
|
|
70
|
+
|
|
71
|
+
The owner's opt-in Neo4j-backed Knowledge Store Provider (the `neo4j` provider). A queryable materialized view of the Knowledge Graph, synced from the file/work-item providers with idempotent MERGE semantics — the file stores stay the source of truth and the write side stays proposals-only. Selected via `KNOWLEDGE_PROVIDER=neo4j`; degrades to the file providers when no Neo4j is reachable. The file providers remain the portfolio default; the graph is a personal default only. See docs/decisions/graph-knowledge-provider.md.
|
|
72
|
+
_Avoid_: Graph database as the default store (it is opt-in and a view, not the source of truth)
|
|
73
|
+
|
|
74
|
+
### Knowledge Promote Sub-Flow
|
|
75
|
+
|
|
76
|
+
The Knowledge Kit's codebase-facing pipeline (the "flow within a flow") that a delivered session's promotion runs through: ingest the session artifacts, distill schema-valid draft decision/vocabulary/learning deltas, link their provenance (PR, merge SHA, session archive, touched topics), and health-check the registry for contradictions with a merge-repair proposal. It is proposals-only — every output is a draft under the session's proposals directory that the promote step applies; the sub-flow never writes docs directly. Invokable standalone and composable from the Builder promote step. See docs/decisions/knowledge-promote-sub-flow.md.
|
|
77
|
+
_Avoid_: Promotion gate as a synonym (the gate is the recorded promote claim; this is the assisted pipeline)
|
|
78
|
+
|
|
69
79
|
### Kontour Resource Contract
|
|
70
80
|
|
|
71
81
|
A versioned Kontour record shape for durable machine-readable configuration, scope, run state, evidence, provider output, and cross-product interchange. Kontour Resource Contracts are the default for new pre-public durable contracts unless a product records why a native shape is clearer.
|
|
@@ -225,7 +235,7 @@ A reason code for a Gate that cannot be evaluated or cannot pass because a requi
|
|
|
225
235
|
|
|
226
236
|
### Probe
|
|
227
237
|
|
|
228
|
-
A Builder Kit Alignment Gate that explores context, challenges assumptions, and records aligned decisions before the process continues. A Probe asks one question at a time, recommends an answer, and uses repository context before asking when the answer can be discovered.
|
|
238
|
+
A Builder Kit Alignment Gate that explores context, challenges assumptions, and records aligned decisions before the process continues. A Probe asks one question at a time, recommends an answer, and uses repository context before asking when the answer can be discovered. As understanding crystallizes, a Probe writes it into durable docs in the same motion: a vocabulary delta into this glossary and a decision delta into the Decision Registry, per [context/contracts/probe-docs-write-contract.md](context/contracts/probe-docs-write-contract.md).
|
|
229
239
|
_Avoid_: Grill, interrogation
|
|
230
240
|
|
|
231
241
|
### Flow
|
|
@@ -328,6 +338,62 @@ The Console overview for global setup, registered projects, cross-project usage,
|
|
|
328
338
|
|
|
329
339
|
The shared tool layer used by the Console, CLI, AI agents, and automation. The Control API owns operations such as reading effective settings, explaining provider resolution, testing provider health, previewing config changes, writing config, inspecting workflow state, and reporting usage or eval outcomes.
|
|
330
340
|
|
|
341
|
+
### Workflow trust state
|
|
342
|
+
|
|
343
|
+
The trust bundle a workflow gate reads to decide whether to advance — claims, evidence, verification events, and derived status expressed as a Hachure Trust Bundle — so gates consume inspectable trust state rather than raw tool output. Provenance lives in frozen ADRs; the subject is open in the Decision Registry as [docs/decisions/workflow-trust-state.md](docs/decisions/workflow-trust-state.md).
|
|
344
|
+
|
|
345
|
+
### TypeScript-first source policy
|
|
346
|
+
|
|
347
|
+
The policy that Kontour product and runtime source defaults to TypeScript, with narrow JavaScript/MJS exceptions (config/tooling, generated assets, thin launchers, fixtures, historical artifacts) and staged per-repo migration for existing non-TypeScript source. Ratified in the Decision Registry as [docs/decisions/typescript-source-policy.md](docs/decisions/typescript-source-policy.md).
|
|
348
|
+
|
|
349
|
+
### Flow / Skill / Kit / Tool boundary
|
|
350
|
+
|
|
351
|
+
The layering that separates a Flow (workflow semantics) from a Skill (agent-facing procedure), a Flow Kit (installable bundle), and a Tool (an executable operation), so each concern has one home and does not leak into the others. Subject open in the Decision Registry as [docs/decisions/flow-skill-kit-tool-boundary.md](docs/decisions/flow-skill-kit-tool-boundary.md).
|
|
352
|
+
|
|
353
|
+
### Kit operation boundary
|
|
354
|
+
|
|
355
|
+
The rule for what a kit-owned operation may do versus what belongs to the core, keeping kit operations scoped to their capability and free of core enforcement responsibilities. Subject open in the Decision Registry as [docs/decisions/kit-operation-boundary.md](docs/decisions/kit-operation-boundary.md).
|
|
356
|
+
|
|
357
|
+
### Hook core/kit boundary
|
|
358
|
+
|
|
359
|
+
The division between canonical hook behavior owned by the core and hook contributions owned by kits, so enforcement hooks have a single authoritative implementation. Subject open in the Decision Registry as [docs/decisions/hook-core-kit-boundary.md](docs/decisions/hook-core-kit-boundary.md).
|
|
360
|
+
|
|
361
|
+
### MCP posture
|
|
362
|
+
|
|
363
|
+
Flow Agents' stance on the Model Context Protocol: enforcement stays in hooks, Surface owns any MCP projection, and no MCP configuration is auto-injected into a runtime. Subject open in the Decision Registry as [docs/decisions/mcp-posture.md](docs/decisions/mcp-posture.md).
|
|
364
|
+
|
|
365
|
+
### Agent coordination
|
|
366
|
+
|
|
367
|
+
How concurrent agents avoid stepping on each other's work, modeled as Hachure liveness claims plus assignment leases with stale-claim takeover, so a work item's holder is advisory-visible and reclaimable when stale. Subject open in the Decision Registry as [docs/decisions/agent-coordination.md](docs/decisions/agent-coordination.md).
|
|
368
|
+
|
|
369
|
+
### Context lifecycle
|
|
370
|
+
|
|
371
|
+
The lifecycle of an agent's working context — workflow-boundary compaction, freshness-gated reuse, and the split between durable learnings and ephemeral context — so context is refreshed rather than silently stale. Subject open in the Decision Registry as [docs/decisions/context-lifecycle.md](docs/decisions/context-lifecycle.md).
|
|
372
|
+
|
|
373
|
+
### Core vs domain kit boundary
|
|
374
|
+
|
|
375
|
+
The generic/kit boundary that keeps the Flow Agents core domain-agnostic while domain behavior lives in kits, so the core carries no kit-specific knowledge. Subject open in the Decision Registry as [docs/decisions/core-domain-kit-boundary.md](docs/decisions/core-domain-kit-boundary.md).
|
|
376
|
+
|
|
377
|
+
### Flow / Flow Agents boundary
|
|
378
|
+
|
|
379
|
+
The reconciled division of responsibility between Flow (the workflow engine consumed for enforcement) and Flow Agents (the product that consumes it), so each owns a distinct layer without duplicating the other. Subject open in the Decision Registry as [docs/decisions/flow-flow-agents-boundary.md](docs/decisions/flow-flow-agents-boundary.md).
|
|
380
|
+
|
|
381
|
+
### Three-hard-boundary model
|
|
382
|
+
|
|
383
|
+
The FlowDefinition-driven, kit-agnostic model that names the three hard boundaries the core enforces, unifying the individual boundary decisions into one architecture. Subject open in the Decision Registry as [docs/decisions/three-hard-boundary-model.md](docs/decisions/three-hard-boundary-model.md).
|
|
384
|
+
|
|
385
|
+
### Anti-gaming trust security
|
|
386
|
+
|
|
387
|
+
The layered-defense trust security model that assumes the local agent can be gamed and anchors enforcement in an external CI check, freezing the local shell-parsing heuristics and routing new enforcement to the CI anchor. Subject open in the Decision Registry as [docs/decisions/anti-gaming-trust-security.md](docs/decisions/anti-gaming-trust-security.md).
|
|
388
|
+
|
|
389
|
+
### Kit dependency ownership
|
|
390
|
+
|
|
391
|
+
The rule for which layer owns a kit's runtime dependencies, keeping dependency declaration and installation with the kit that needs them rather than the core. Subject open in the Decision Registry as [docs/decisions/kit-dependency-ownership.md](docs/decisions/kit-dependency-ownership.md).
|
|
392
|
+
|
|
393
|
+
### Trust-reconcile and delivery reconciliation
|
|
394
|
+
|
|
395
|
+
The CI-anchored reconciliation of a session's trust claims against a manifest — classifying command, session-local, and attested claims, honoring governed waivers — and the fail-closed delivery reconciliation that blocks publication on unreconciled residue unless an exemption is recorded. Subject open in the Decision Registry as [docs/decisions/trust-reconcile.md](docs/decisions/trust-reconcile.md).
|
|
396
|
+
|
|
331
397
|
### Model Routing
|
|
332
398
|
|
|
333
399
|
The policy that maps a delegate role name (such as `delegate-mechanical`, `delegate-implementation`, `delegate-design`, `orchestrator`, `extraction-default`) to a specific `model@provider` ref. Model Routing is data, not code: it lives in `.datum/config.json` (read by the `@kontourai/datum` registry, schema `datum.schema.json`) and never in generated files or per-agent frontmatter. The orchestrator resolves the role at delegation time (`datum resolve <role> --json`) and passes the resolved model explicitly when spawning each delegate. See [context/contracts/execution-contract.md](context/contracts/execution-contract.md) § Delegation: Model Routing and [docs/decisions/model-routing.md](docs/decisions/model-routing.md).
|
package/README.md
CHANGED
|
@@ -217,6 +217,10 @@ See [Repository Structure](docs/repository-structure.md) for the canonical map.
|
|
|
217
217
|
- `kits/`, `schemas/`, `packaging/`, `evals/`, `docs/`, and `integrations/` hold Flow Kit assets, contracts, package metadata, evals, durable docs, and optional integrations.
|
|
218
218
|
- `dist/`, `build/`, and `_site/` are generated output. Local `.kontourai/`, `.flow-agents/`, `.codex/`, `.claude/`, telemetry, promptfoo, Veritas, and cache directories stay ignored; durable outcomes belong in docs, source, schemas, or provider records.
|
|
219
219
|
|
|
220
|
+
## Graph provider (opt-in)
|
|
221
|
+
|
|
222
|
+
The Knowledge Kit ships an optional `neo4j` graph provider — the owner's opt-in personal default, while the file providers remain the portfolio default. It is a queryable **materialized view** synced from the file/work-item stores (which stay the source of truth), with Cypher-backed health/query verbs when selected and graceful degradation to the file providers when no Neo4j is reachable (never a hard dependency). Opt in with `KNOWLEDGE_PROVIDER=neo4j` and see [Graph provider (opt-in)](kits/knowledge/docs/README.md#graph-provider-opt-in) for the `docker run` one-liner, env vars, sync command, and an example Cypher session.
|
|
223
|
+
|
|
220
224
|
## Documentation
|
|
221
225
|
|
|
222
226
|
The [GitHub Pages site](https://kontourai.github.io/flow-agents/) is the product overview and quickstart. The repository docs are the developer reference for contracts, workflow behavior, evals, and integration boundaries. For the gate semantics underneath — definitions, runs, evidence, route-back — read the [Kontour Flow documentation](https://kontourai.github.io/flow/).
|
|
@@ -1 +1,144 @@
|
|
|
1
|
+
export type ActorStruct = {
|
|
2
|
+
runtime: string;
|
|
3
|
+
session_id: string;
|
|
4
|
+
host: string;
|
|
5
|
+
human?: string | null;
|
|
6
|
+
};
|
|
7
|
+
type AssignmentClaimRecordStatus = "claimed" | "released" | "superseded";
|
|
8
|
+
type AssignmentAuditEntry = {
|
|
9
|
+
at: string;
|
|
10
|
+
transition: "claim" | "release" | "supersede";
|
|
11
|
+
from_actor?: ActorStruct | null;
|
|
12
|
+
to_actor?: ActorStruct | null;
|
|
13
|
+
reason?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The versioned claim-record shape from the contract doc's "Versioned claim-record format"
|
|
17
|
+
* section (Design Decision 2). `schema_version` is bumped only on an incompatible change, per
|
|
18
|
+
* artifact-contract.md's existing sidecar rule.
|
|
19
|
+
*
|
|
20
|
+
* `actor_key` (F1 fix, fix-plan iteration 1, HIGH — additive field, schema_version unchanged):
|
|
21
|
+
* the canonical `resolveActor(env).actor` string for the claiming actor — the SAME flat/bare
|
|
22
|
+
* token every other tool (`liveness whoami`, `liveness claim --actor`, per-actor current.json,
|
|
23
|
+
* pull-work's `--self-actor`) already uses. Optional so every pre-fix record and every #290 eval
|
|
24
|
+
* fixture with no `actor_key` still parses; `computeEffectiveState` falls back to
|
|
25
|
+
* `serializeActor(record.actor)` (today's behavior) whenever it's absent. Present, it is the ONLY
|
|
26
|
+
* correct self-recognition/liveness-join key — see computeEffectiveState's holderActorKey.
|
|
27
|
+
*/
|
|
28
|
+
export type AssignmentClaimRecord = {
|
|
29
|
+
schema_version: "1.0";
|
|
30
|
+
role: "AssignmentClaimRecord";
|
|
31
|
+
subject_id: string;
|
|
32
|
+
actor: ActorStruct;
|
|
33
|
+
actor_key?: string;
|
|
34
|
+
claimed_at: string;
|
|
35
|
+
ttl_seconds: number;
|
|
36
|
+
branch: string;
|
|
37
|
+
artifact_dir: string;
|
|
38
|
+
status: AssignmentClaimRecordStatus;
|
|
39
|
+
audit_trail?: AssignmentAuditEntry[];
|
|
40
|
+
};
|
|
41
|
+
export type FreshHolder = {
|
|
42
|
+
actor: string;
|
|
43
|
+
lastAt: string;
|
|
44
|
+
ttlSeconds: number;
|
|
45
|
+
fresh: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type EffectiveState = "held" | "reclaimable" | "human-held" | "free";
|
|
48
|
+
/** Provider-neutral assignment-layer read, before any liveness join (contract doc's status()). */
|
|
49
|
+
export type AssignmentStatus = {
|
|
50
|
+
subject_id: string;
|
|
51
|
+
provider: "local-file" | "github";
|
|
52
|
+
assignee: string | null;
|
|
53
|
+
record: AssignmentClaimRecord | null;
|
|
54
|
+
has_claim_label?: boolean;
|
|
55
|
+
};
|
|
56
|
+
export declare function assignmentFilePath(artifactRoot: string, subjectId: string): string;
|
|
57
|
+
export declare function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null;
|
|
58
|
+
export declare function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void;
|
|
59
|
+
/**
|
|
60
|
+
* F1 fix (fix-plan iteration 1, CRITICAL): claimLocalFile/releaseLocalFile/supersedeLocalFile were
|
|
61
|
+
* a plain read -> compare-actor -> write with no lock, so two concurrently-launched OS processes
|
|
62
|
+
* could both read "no conflicting claim" before either wrote, and the second write would silently
|
|
63
|
+
* clobber the first with zero error and zero audit-trail entry for the loser (reproduced 29/40
|
|
64
|
+
* races against the built CLI). This mirrors the EXACT mechanism `withLock` already uses in
|
|
65
|
+
* workflow-sidecar.ts:908 for the same class of shared-state mutation — atomic `fs.mkdirSync`
|
|
66
|
+
* lockdir create as the mutual-exclusion primitive, EEXIST-spin with a staleness-reclaim check
|
|
67
|
+
* (a lock directory older than the stale threshold is presumed abandoned by a crashed process and
|
|
68
|
+
* is reclaimed rather than waited on forever) and a bounded deadline, `finally` rmSync release —
|
|
69
|
+
* as a small LOCAL helper (not a cross-import of that private function, which would pull the
|
|
70
|
+
* entire workflow-sidecar module in for one primitive). Deliberately synchronous (sleepSync's
|
|
71
|
+
* Atomics.wait spin, not setTimeout/await) so claim/release/supersede can stay sync `number`
|
|
72
|
+
* -returning functions and the CLI dispatcher (src/cli.ts, `number | Promise<number>`) does not
|
|
73
|
+
* need any ripple to async. On lock-acquire failure (any error other than a live contested lock,
|
|
74
|
+
* or a timeout waiting one out) this THROWS — never a silent no-op — "fail loud, never fail-open"
|
|
75
|
+
* (artifact-contract.md). Wrap the ENTIRE read-modify-write body (the existing-claim check AND
|
|
76
|
+
* the write) of all three local-file mutators in this, since all three mutate the same record
|
|
77
|
+
* file for a given subject.
|
|
78
|
+
*/
|
|
79
|
+
export declare function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () => T): T;
|
|
80
|
+
/**
|
|
81
|
+
* The assignment ⋈ liveness join (contract doc's "assignment ⋈ liveness join" section, ADR 0021
|
|
82
|
+
* §1). Pure function: `{ assignment, freshHoldersList, selfActor, nowMs }` -> one of five
|
|
83
|
+
* effective states (held/reclaimable/human-held/free — "held" covers both the plain and
|
|
84
|
+
* assignment-lagging rows, matching the contract table's own repeated "held" label).
|
|
85
|
+
*
|
|
86
|
+
* The human-assignee gate (AC11, Design Decision 3) reads `record.actor.human` being *present*,
|
|
87
|
+
* never a username heuristic — an idle human assignment is always `human-held`, regardless of
|
|
88
|
+
* idle duration, and is never auto-reclaimable by this function.
|
|
89
|
+
*
|
|
90
|
+
* `nowMs` (F3 fix, fix-plan iteration 1) is the SAME resolved "now" the caller already threads
|
|
91
|
+
* into `freshHolders()` (the `--now` override, when passed, else `Date.now()`) — passing it
|
|
92
|
+
* through here too means `--now` deterministically governs idle_days as well as liveness
|
|
93
|
+
* freshness, rather than idle_days silently reading the real wall clock regardless of `--now`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function computeEffectiveState(assignment: AssignmentStatus, freshHoldersList: FreshHolder[], selfActor: string | undefined, nowMs: number): {
|
|
96
|
+
effective_state: EffectiveState;
|
|
97
|
+
reason: string;
|
|
98
|
+
holder?: {
|
|
99
|
+
actor?: string;
|
|
100
|
+
assignee?: string | null;
|
|
101
|
+
idle_days?: number | null;
|
|
102
|
+
last_at?: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside claimLocalFile's
|
|
107
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's ownership
|
|
108
|
+
* guard (workflow-sidecar.ts, Wave 2) can reuse the EXACT same claim logic — same-actor idempotent
|
|
109
|
+
* refresh, different-actor throw, atomic write under withSubjectLock — rather than reimplementing
|
|
110
|
+
* a second, parallel claim path. claimLocalFile (CLI wrapper, below) is now a thin
|
|
111
|
+
* parse-args/print-envelope shell around this.
|
|
112
|
+
*/
|
|
113
|
+
export declare function performLocalClaim(artifactRoot: string, subjectId: string, actor: ActorStruct, opts: {
|
|
114
|
+
ttlSeconds: number;
|
|
115
|
+
branch: string;
|
|
116
|
+
artifactDir: string;
|
|
117
|
+
reason?: string;
|
|
118
|
+
actorKey?: string;
|
|
119
|
+
}): AssignmentClaimRecord;
|
|
120
|
+
/**
|
|
121
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
|
|
122
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
|
|
123
|
+
* `--supersede-stale` takeover path (workflow-sidecar.ts, Wave 2) can reuse the EXACT same
|
|
124
|
+
* supersede logic — from-actor holder verification, ttl/branch/artifact_dir carry-forward,
|
|
125
|
+
* audit-trail append, atomic write under withSubjectLock — rather than reimplementing a second,
|
|
126
|
+
* parallel supersede path. supersedeLocalFile (CLI wrapper, below) is now a thin
|
|
127
|
+
* parse-args/print-envelope shell around this.
|
|
128
|
+
*/
|
|
129
|
+
export declare function performLocalSupersede(artifactRoot: string, subjectId: string, fromActor: ActorStruct, toActor: ActorStruct, opts?: {
|
|
130
|
+
ttlSeconds?: number;
|
|
131
|
+
branch?: string;
|
|
132
|
+
artifactDir?: string;
|
|
133
|
+
reason?: string;
|
|
134
|
+
actorKey?: string;
|
|
135
|
+
}): AssignmentClaimRecord;
|
|
136
|
+
/**
|
|
137
|
+
* Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
|
|
138
|
+
* mirrored exactly so ensure-session's ownership guard (workflow-sidecar.ts, Wave 2) derives an
|
|
139
|
+
* AssignmentStatus identically to the `assignment-provider status` CLI command — a single
|
|
140
|
+
* implementation, not a second parallel local-file read.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readLocalAssignmentStatus(artifactRoot: string, subjectId: string): AssignmentStatus;
|
|
1
143
|
export declare function main(argv?: string[]): number;
|
|
144
|
+
export {};
|
|
@@ -64,22 +64,29 @@ function loadActorStructFromFile(file) {
|
|
|
64
64
|
* fixture-friendly path (used by evals and any caller that already knows its own struct);
|
|
65
65
|
* when omitted, auto-derive from the live environment via the shared resolver, mirroring
|
|
66
66
|
* (never forking) the exact struct fields serializeActor() already defines.
|
|
67
|
+
*
|
|
68
|
+
* F1 fix (fix-plan iteration 1, HIGH): also returns `actorKey` — set to the canonical
|
|
69
|
+
* `resolveActor(env).actor` string ONLY on the auto-derive path (pull-work's real path, and any
|
|
70
|
+
* other caller with no --actor-json), so a claim made via `assignment-provider claim` and one
|
|
71
|
+
* made via ensure-session share the same canonical key. `--actor-json` explicit fixtures leave
|
|
72
|
+
* `actorKey` unset — `performLocalClaim`/`performLocalSupersede` then fall back to
|
|
73
|
+
* `serializeActor(actor)` for the record's `actor_key`, preserving existing fixture behavior.
|
|
67
74
|
*/
|
|
68
75
|
function loadActorStruct(args) {
|
|
69
76
|
const actorJsonPath = flagString(args.flags, "actor-json");
|
|
70
77
|
if (actorJsonPath)
|
|
71
|
-
return loadActorStructFromFile(actorJsonPath);
|
|
78
|
+
return { actor: loadActorStructFromFile(actorJsonPath) };
|
|
72
79
|
const helper = loadActorIdentityHelper();
|
|
73
80
|
const resolved = helper.resolveActor(process.env);
|
|
74
81
|
if (helper.isUnresolvedActor(resolved.actor))
|
|
75
82
|
throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
|
|
76
|
-
return { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null };
|
|
83
|
+
return { actor: { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null }, actorKey: resolved.actor };
|
|
77
84
|
}
|
|
78
|
-
function assignmentFilePath(artifactRoot, subjectId) {
|
|
85
|
+
export function assignmentFilePath(artifactRoot, subjectId) {
|
|
79
86
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
80
87
|
return path.join(artifactRoot, "assignment", `${sanitized}.json`);
|
|
81
88
|
}
|
|
82
|
-
function readLocalRecord(artifactRoot, subjectId) {
|
|
89
|
+
export function readLocalRecord(artifactRoot, subjectId) {
|
|
83
90
|
const file = assignmentFilePath(artifactRoot, subjectId);
|
|
84
91
|
if (!fs.existsSync(file))
|
|
85
92
|
return null;
|
|
@@ -99,7 +106,7 @@ function readLocalRecord(artifactRoot, subjectId) {
|
|
|
99
106
|
throw new Error(`${file}: unsupported schema_version ${String(record.schema_version)}`);
|
|
100
107
|
return record;
|
|
101
108
|
}
|
|
102
|
-
function writeLocalRecord(artifactRoot, subjectId, record) {
|
|
109
|
+
export function writeLocalRecord(artifactRoot, subjectId, record) {
|
|
103
110
|
// writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
|
|
104
111
|
// propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
|
|
105
112
|
// never fail open (artifact-contract.md).
|
|
@@ -142,7 +149,7 @@ function subjectLockDir(artifactRoot, subjectId) {
|
|
|
142
149
|
* the write) of all three local-file mutators in this, since all three mutate the same record
|
|
143
150
|
* file for a given subject.
|
|
144
151
|
*/
|
|
145
|
-
function withSubjectLock(artifactRoot, subjectId, body) {
|
|
152
|
+
export function withSubjectLock(artifactRoot, subjectId, body) {
|
|
146
153
|
const lockDir = subjectLockDir(artifactRoot, subjectId);
|
|
147
154
|
const staleMs = Number(process.env.FLOW_AGENTS_ASSIGNMENT_STALE_LOCK_MS ?? 5 * 60 * 1000);
|
|
148
155
|
const deadline = Date.now() + 30000;
|
|
@@ -196,7 +203,7 @@ function withSubjectLock(artifactRoot, subjectId, body) {
|
|
|
196
203
|
* through here too means `--now` deterministically governs idle_days as well as liveness
|
|
197
204
|
* freshness, rather than idle_days silently reading the real wall clock regardless of `--now`.
|
|
198
205
|
*/
|
|
199
|
-
function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
|
|
206
|
+
export function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
|
|
200
207
|
const record = assignment.record && assignment.record.status === "claimed" ? assignment.record : null;
|
|
201
208
|
const isAssigned = Boolean(assignment.assignee) || Boolean(record);
|
|
202
209
|
if (!isAssigned) {
|
|
@@ -222,7 +229,14 @@ function computeEffectiveState(assignment, freshHoldersList, selfActor, nowMs) {
|
|
|
222
229
|
// human's work (Design Decision 3 / ADR 0021 §6's "never auto-reclaim" non-goal).
|
|
223
230
|
return { effective_state: "human-held", reason: "assignee_without_claim_record", holder: { assignee: assignment.assignee } };
|
|
224
231
|
}
|
|
225
|
-
|
|
232
|
+
// F1 fix (fix-plan iteration 1, HIGH): prefer the canonical actor_key over re-serializing
|
|
233
|
+
// record.actor. record.actor_key IS resolveActor(env).actor for records written by the fixed
|
|
234
|
+
// performLocalClaim/performLocalSupersede paths (below) — the same flat/bare string liveness
|
|
235
|
+
// whoami, `liveness claim --actor`, per-actor current.json, and pull-work's --self-actor all
|
|
236
|
+
// use — so for an explicit-override actor the self-check and the liveness join now agree with
|
|
237
|
+
// every other tool. BACK-COMPAT: records with no actor_key (every pre-fix record, every #290
|
|
238
|
+
// eval fixture) fall back to serializeActor(record.actor) exactly as before this fix.
|
|
239
|
+
const holderActorKey = record.actor_key || loadActorIdentityHelper().serializeActor(record.actor);
|
|
226
240
|
if (selfActor && holderActorKey === selfActor)
|
|
227
241
|
return { effective_state: "held", reason: "self_is_holder", holder: { actor: holderActorKey } };
|
|
228
242
|
const fresh = freshHoldersList.find((holder) => holder.actor === holderActorKey);
|
|
@@ -349,22 +363,17 @@ function loadLivenessInputs(args) {
|
|
|
349
363
|
}
|
|
350
364
|
// ─── local-file: claim | release | supersede (the durable-write path; real I/O by design — no
|
|
351
365
|
// external mutation to defer to a skill for this provider kind, per Design Decision 1) ─────────
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside claimLocalFile's
|
|
368
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's ownership
|
|
369
|
+
* guard (workflow-sidecar.ts, Wave 2) can reuse the EXACT same claim logic — same-actor idempotent
|
|
370
|
+
* refresh, different-actor throw, atomic write under withSubjectLock — rather than reimplementing
|
|
371
|
+
* a second, parallel claim path. claimLocalFile (CLI wrapper, below) is now a thin
|
|
372
|
+
* parse-args/print-envelope shell around this.
|
|
373
|
+
*/
|
|
374
|
+
export function performLocalClaim(artifactRoot, subjectId, actor, opts) {
|
|
360
375
|
const helper = loadActorIdentityHelper();
|
|
361
|
-
const
|
|
362
|
-
const ttlSeconds = Number(ttlSecondsRaw);
|
|
363
|
-
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
|
|
364
|
-
throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
365
|
-
const branch = requireFlag(args, "branch");
|
|
366
|
-
const artifactDir = requireFlag(args, "artifact-dir");
|
|
367
|
-
const reason = flagString(args.flags, "reason") ?? "claim";
|
|
376
|
+
const reason = opts.reason ?? "claim";
|
|
368
377
|
// F1 fix (fix-plan iteration 1, CRITICAL): the existing-claim check AND the write must happen
|
|
369
378
|
// atomically with respect to any other `assignment-provider` invocation on the same subject —
|
|
370
379
|
// see withSubjectLock()'s doc comment for the full rationale.
|
|
@@ -384,18 +393,37 @@ function claimLocalFile(argv) {
|
|
|
384
393
|
role: "AssignmentClaimRecord",
|
|
385
394
|
subject_id: subjectId,
|
|
386
395
|
actor,
|
|
396
|
+
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
387
397
|
claimed_at: isoNow(),
|
|
388
|
-
ttl_seconds: ttlSeconds,
|
|
389
|
-
branch,
|
|
390
|
-
artifact_dir: artifactDir,
|
|
398
|
+
ttl_seconds: opts.ttlSeconds,
|
|
399
|
+
branch: opts.branch,
|
|
400
|
+
artifact_dir: opts.artifactDir,
|
|
391
401
|
status: "claimed",
|
|
392
402
|
audit_trail: [...(existing?.audit_trail ?? []), { at: isoNow(), transition: "claim", from_actor: null, to_actor: actor, reason }],
|
|
393
403
|
};
|
|
394
404
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
395
|
-
|
|
396
|
-
return 0;
|
|
405
|
+
return record;
|
|
397
406
|
});
|
|
398
407
|
}
|
|
408
|
+
function claimLocalFile(argv) {
|
|
409
|
+
const args = parseArgs(argv);
|
|
410
|
+
const provider = flagString(args.flags, "provider", "local-file");
|
|
411
|
+
if (provider !== "local-file")
|
|
412
|
+
throw new Error(`claim: --provider must be local-file (use render-claim for github); got ${provider}`);
|
|
413
|
+
const artifactRoot = requireFlag(args, "artifact-root");
|
|
414
|
+
const subjectId = requireFlag(args, "subject-id");
|
|
415
|
+
const { actor, actorKey } = loadActorStruct(args);
|
|
416
|
+
const ttlSecondsRaw = flagString(args.flags, "ttl-seconds", "1800") ?? "1800";
|
|
417
|
+
const ttlSeconds = Number(ttlSecondsRaw);
|
|
418
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
|
|
419
|
+
throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
420
|
+
const branch = requireFlag(args, "branch");
|
|
421
|
+
const artifactDir = requireFlag(args, "artifact-dir");
|
|
422
|
+
const reason = flagString(args.flags, "reason") ?? "claim";
|
|
423
|
+
const record = performLocalClaim(artifactRoot, subjectId, actor, { ttlSeconds, branch, artifactDir, reason, actorKey });
|
|
424
|
+
console.log(JSON.stringify({ role: "AssignmentClaimResult", subject_id: subjectId, record }, null, 2));
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
399
427
|
function releaseLocalFile(argv) {
|
|
400
428
|
const args = parseArgs(argv);
|
|
401
429
|
const provider = flagString(args.flags, "provider", "local-file");
|
|
@@ -421,20 +449,18 @@ function releaseLocalFile(argv) {
|
|
|
421
449
|
return 0;
|
|
422
450
|
});
|
|
423
451
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
|
|
454
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
|
|
455
|
+
* `--supersede-stale` takeover path (workflow-sidecar.ts, Wave 2) can reuse the EXACT same
|
|
456
|
+
* supersede logic — from-actor holder verification, ttl/branch/artifact_dir carry-forward,
|
|
457
|
+
* audit-trail append, atomic write under withSubjectLock — rather than reimplementing a second,
|
|
458
|
+
* parallel supersede path. supersedeLocalFile (CLI wrapper, below) is now a thin
|
|
459
|
+
* parse-args/print-envelope shell around this.
|
|
460
|
+
*/
|
|
461
|
+
export function performLocalSupersede(artifactRoot, subjectId, fromActor, toActor, opts = {}) {
|
|
431
462
|
const helper = loadActorIdentityHelper();
|
|
432
|
-
const
|
|
433
|
-
const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
|
|
434
|
-
const reason = flagString(args.flags, "reason") ?? "supersede";
|
|
435
|
-
const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
|
|
436
|
-
const branchOverride = flagString(args.flags, "branch");
|
|
437
|
-
const artifactDirOverride = flagString(args.flags, "artifact-dir");
|
|
463
|
+
const reason = opts.reason ?? "supersede";
|
|
438
464
|
// F1 fix (fix-plan iteration 1, CRITICAL): supersede mutates the same record file claim/release
|
|
439
465
|
// do, under the same per-subject lock (see withSubjectLock()'s doc comment).
|
|
440
466
|
return withSubjectLock(artifactRoot, subjectId, () => {
|
|
@@ -444,7 +470,7 @@ function supersedeLocalFile(argv) {
|
|
|
444
470
|
if (helper.serializeActor(existing.actor) !== helper.serializeActor(fromActor)) {
|
|
445
471
|
throw new Error(`--from-actor-json does not match the current holder (${helper.serializeActor(existing.actor)}); refusing to supersede a claim held by someone else`);
|
|
446
472
|
}
|
|
447
|
-
const ttlSecondsRaw =
|
|
473
|
+
const ttlSecondsRaw = opts.ttlSeconds != null ? String(opts.ttlSeconds) : String(existing.ttl_seconds);
|
|
448
474
|
const ttlSeconds = Number(ttlSecondsRaw);
|
|
449
475
|
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
|
|
450
476
|
throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
@@ -453,18 +479,40 @@ function supersedeLocalFile(argv) {
|
|
|
453
479
|
role: "AssignmentClaimRecord",
|
|
454
480
|
subject_id: subjectId,
|
|
455
481
|
actor: toActor,
|
|
482
|
+
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
456
483
|
claimed_at: isoNow(),
|
|
457
484
|
ttl_seconds: ttlSeconds,
|
|
458
|
-
branch:
|
|
459
|
-
artifact_dir:
|
|
485
|
+
branch: opts.branch ?? existing.branch,
|
|
486
|
+
artifact_dir: opts.artifactDir ?? existing.artifact_dir,
|
|
460
487
|
status: "claimed",
|
|
461
488
|
audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "supersede", from_actor: fromActor, to_actor: toActor, reason }],
|
|
462
489
|
};
|
|
463
490
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
464
|
-
|
|
465
|
-
return 0;
|
|
491
|
+
return record;
|
|
466
492
|
});
|
|
467
493
|
}
|
|
494
|
+
function supersedeLocalFile(argv) {
|
|
495
|
+
const args = parseArgs(argv);
|
|
496
|
+
const provider = flagString(args.flags, "provider", "local-file");
|
|
497
|
+
if (provider !== "local-file")
|
|
498
|
+
throw new Error(`supersede: --provider must be local-file (use render-supersede for github); got ${provider}`);
|
|
499
|
+
const artifactRoot = requireFlag(args, "artifact-root");
|
|
500
|
+
const subjectId = requireFlag(args, "subject-id");
|
|
501
|
+
const fromActor = loadActorStructFromFile(requireFlag(args, "from-actor-json"));
|
|
502
|
+
const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
|
|
503
|
+
const reason = flagString(args.flags, "reason") ?? "supersede";
|
|
504
|
+
const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
|
|
505
|
+
const branchOverride = flagString(args.flags, "branch");
|
|
506
|
+
const artifactDirOverride = flagString(args.flags, "artifact-dir");
|
|
507
|
+
const record = performLocalSupersede(artifactRoot, subjectId, fromActor, toActor, {
|
|
508
|
+
ttlSeconds: ttlSecondsOverride != null ? Number(ttlSecondsOverride) : undefined,
|
|
509
|
+
branch: branchOverride ?? undefined,
|
|
510
|
+
artifactDir: artifactDirOverride ?? undefined,
|
|
511
|
+
reason,
|
|
512
|
+
});
|
|
513
|
+
console.log(JSON.stringify({ role: "AssignmentSupersedeResult", subject_id: subjectId, record }, null, 2));
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
468
516
|
// ─── GitHub: render-claim | render-release | render-supersede (render, don't execute — Design
|
|
469
517
|
// Decision 1). Pure functions: no I/O beyond reading --input-json/--actor-json. Never invoke
|
|
470
518
|
// `gh` (or any process) here — the calling skill runs the emitted argv verbatim. ────────────────
|
|
@@ -624,6 +672,17 @@ function renderSupersede(argv) {
|
|
|
624
672
|
return 0;
|
|
625
673
|
}
|
|
626
674
|
// ─── status | list (both provider kinds) ────────────────────────────────────────────────────
|
|
675
|
+
/**
|
|
676
|
+
* Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
|
|
677
|
+
* mirrored exactly so ensure-session's ownership guard (workflow-sidecar.ts, Wave 2) derives an
|
|
678
|
+
* AssignmentStatus identically to the `assignment-provider status` CLI command — a single
|
|
679
|
+
* implementation, not a second parallel local-file read.
|
|
680
|
+
*/
|
|
681
|
+
export function readLocalAssignmentStatus(artifactRoot, subjectId) {
|
|
682
|
+
const record = readLocalRecord(artifactRoot, subjectId);
|
|
683
|
+
const active = record && record.status === "claimed" ? record : null;
|
|
684
|
+
return { subject_id: subjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
|
|
685
|
+
}
|
|
627
686
|
function statusCommand(argv) {
|
|
628
687
|
const args = parseArgs(argv);
|
|
629
688
|
const provider = requireFlag(args, "provider");
|
|
@@ -633,9 +692,7 @@ function statusCommand(argv) {
|
|
|
633
692
|
const artifactRoot = requireFlag(args, "artifact-root");
|
|
634
693
|
if (!requestedSubjectId)
|
|
635
694
|
throw new Error("--subject-id is required for status --provider local-file");
|
|
636
|
-
|
|
637
|
-
const active = record && record.status === "claimed" ? record : null;
|
|
638
|
-
assignment = { subject_id: requestedSubjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
|
|
695
|
+
assignment = readLocalAssignmentStatus(artifactRoot, requestedSubjectId);
|
|
639
696
|
}
|
|
640
697
|
else if (provider === "github") {
|
|
641
698
|
const issueJsonPath = requireFlag(args, "issue-json");
|
|
@@ -145,7 +145,7 @@ type SurfaceModule = {
|
|
|
145
145
|
* @param critiques Critique objects (from critique.json .critiques array)
|
|
146
146
|
* @param commandLog Optional parsed command-log.jsonl entries (capture-authoritative fold)
|
|
147
147
|
*/
|
|
148
|
-
export declare function buildTrustBundle(slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], commandLog?: AnyObj[], flowAgentsDir?: string): Promise<AnyObj | null>;
|
|
148
|
+
export declare function buildTrustBundle(slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], commandLog?: AnyObj[], flowAgentsDir?: string, actorKey?: string): Promise<AnyObj | null>;
|
|
149
149
|
/**
|
|
150
150
|
* Fail-open wrapper: builds (via Surface), validates, and writes a trust.bundle.
|
|
151
151
|
* Accepts raw check/criterion/critique inputs directly (ADR 0010 Phase 4a).
|
|
@@ -159,7 +159,7 @@ export declare function buildTrustBundle(slug: string, timestamp: string, checks
|
|
|
159
159
|
* @param criteria Acceptance criteria objects (same as buildTrustBundle)
|
|
160
160
|
* @param critiques Critique objects (same as buildTrustBundle)
|
|
161
161
|
*/
|
|
162
|
-
export declare function writeTrustBundle(dir: string, slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[]): Promise<{
|
|
162
|
+
export declare function writeTrustBundle(dir: string, slug: string, timestamp: string, checks: AnyObj[], criteria: AnyObj[], critiques: AnyObj[], actorKey?: string): Promise<{
|
|
163
163
|
written: boolean;
|
|
164
164
|
errors: string[];
|
|
165
165
|
}>;
|