@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
|
@@ -24,7 +24,7 @@ import { readJson, writeJson, isoNow } from "../lib/fs.js";
|
|
|
24
24
|
|
|
25
25
|
type AnyObj = Record<string, unknown>;
|
|
26
26
|
|
|
27
|
-
type ActorStruct = {
|
|
27
|
+
export type ActorStruct = {
|
|
28
28
|
runtime: string;
|
|
29
29
|
session_id: string;
|
|
30
30
|
host: string;
|
|
@@ -45,12 +45,21 @@ type AssignmentAuditEntry = {
|
|
|
45
45
|
* The versioned claim-record shape from the contract doc's "Versioned claim-record format"
|
|
46
46
|
* section (Design Decision 2). `schema_version` is bumped only on an incompatible change, per
|
|
47
47
|
* artifact-contract.md's existing sidecar rule.
|
|
48
|
+
*
|
|
49
|
+
* `actor_key` (F1 fix, fix-plan iteration 1, HIGH — additive field, schema_version unchanged):
|
|
50
|
+
* the canonical `resolveActor(env).actor` string for the claiming actor — the SAME flat/bare
|
|
51
|
+
* token every other tool (`liveness whoami`, `liveness claim --actor`, per-actor current.json,
|
|
52
|
+
* pull-work's `--self-actor`) already uses. Optional so every pre-fix record and every #290 eval
|
|
53
|
+
* fixture with no `actor_key` still parses; `computeEffectiveState` falls back to
|
|
54
|
+
* `serializeActor(record.actor)` (today's behavior) whenever it's absent. Present, it is the ONLY
|
|
55
|
+
* correct self-recognition/liveness-join key — see computeEffectiveState's holderActorKey.
|
|
48
56
|
*/
|
|
49
|
-
type AssignmentClaimRecord = {
|
|
57
|
+
export type AssignmentClaimRecord = {
|
|
50
58
|
schema_version: "1.0";
|
|
51
59
|
role: "AssignmentClaimRecord";
|
|
52
60
|
subject_id: string;
|
|
53
61
|
actor: ActorStruct;
|
|
62
|
+
actor_key?: string;
|
|
54
63
|
claimed_at: string;
|
|
55
64
|
ttl_seconds: number;
|
|
56
65
|
branch: string;
|
|
@@ -59,12 +68,12 @@ type AssignmentClaimRecord = {
|
|
|
59
68
|
audit_trail?: AssignmentAuditEntry[];
|
|
60
69
|
};
|
|
61
70
|
|
|
62
|
-
type FreshHolder = { actor: string; lastAt: string; ttlSeconds: number; fresh: boolean };
|
|
71
|
+
export type FreshHolder = { actor: string; lastAt: string; ttlSeconds: number; fresh: boolean };
|
|
63
72
|
|
|
64
|
-
type EffectiveState = "held" | "reclaimable" | "human-held" | "free";
|
|
73
|
+
export type EffectiveState = "held" | "reclaimable" | "human-held" | "free";
|
|
65
74
|
|
|
66
75
|
/** Provider-neutral assignment-layer read, before any liveness join (contract doc's status()). */
|
|
67
|
-
type AssignmentStatus = {
|
|
76
|
+
export type AssignmentStatus = {
|
|
68
77
|
subject_id: string;
|
|
69
78
|
provider: "local-file" | "github";
|
|
70
79
|
assignee: string | null;
|
|
@@ -176,22 +185,29 @@ function loadActorStructFromFile(file: string): ActorStruct {
|
|
|
176
185
|
* fixture-friendly path (used by evals and any caller that already knows its own struct);
|
|
177
186
|
* when omitted, auto-derive from the live environment via the shared resolver, mirroring
|
|
178
187
|
* (never forking) the exact struct fields serializeActor() already defines.
|
|
188
|
+
*
|
|
189
|
+
* F1 fix (fix-plan iteration 1, HIGH): also returns `actorKey` — set to the canonical
|
|
190
|
+
* `resolveActor(env).actor` string ONLY on the auto-derive path (pull-work's real path, and any
|
|
191
|
+
* other caller with no --actor-json), so a claim made via `assignment-provider claim` and one
|
|
192
|
+
* made via ensure-session share the same canonical key. `--actor-json` explicit fixtures leave
|
|
193
|
+
* `actorKey` unset — `performLocalClaim`/`performLocalSupersede` then fall back to
|
|
194
|
+
* `serializeActor(actor)` for the record's `actor_key`, preserving existing fixture behavior.
|
|
179
195
|
*/
|
|
180
|
-
function loadActorStruct(args: ParsedArgs): ActorStruct {
|
|
196
|
+
function loadActorStruct(args: ParsedArgs): { actor: ActorStruct; actorKey?: string } {
|
|
181
197
|
const actorJsonPath = flagString(args.flags, "actor-json");
|
|
182
|
-
if (actorJsonPath) return loadActorStructFromFile(actorJsonPath);
|
|
198
|
+
if (actorJsonPath) return { actor: loadActorStructFromFile(actorJsonPath) };
|
|
183
199
|
const helper = loadActorIdentityHelper();
|
|
184
200
|
const resolved = helper.resolveActor(process.env);
|
|
185
201
|
if (helper.isUnresolvedActor(resolved.actor)) throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
|
|
186
|
-
return { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null };
|
|
202
|
+
return { actor: { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null }, actorKey: resolved.actor };
|
|
187
203
|
}
|
|
188
204
|
|
|
189
|
-
function assignmentFilePath(artifactRoot: string, subjectId: string): string {
|
|
205
|
+
export function assignmentFilePath(artifactRoot: string, subjectId: string): string {
|
|
190
206
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
191
207
|
return path.join(artifactRoot, "assignment", `${sanitized}.json`);
|
|
192
208
|
}
|
|
193
209
|
|
|
194
|
-
function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null {
|
|
210
|
+
export function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null {
|
|
195
211
|
const file = assignmentFilePath(artifactRoot, subjectId);
|
|
196
212
|
if (!fs.existsSync(file)) return null;
|
|
197
213
|
let data: unknown;
|
|
@@ -208,7 +224,7 @@ function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentCla
|
|
|
208
224
|
return record;
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void {
|
|
227
|
+
export function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void {
|
|
212
228
|
// writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
|
|
213
229
|
// propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
|
|
214
230
|
// never fail open (artifact-contract.md).
|
|
@@ -254,7 +270,7 @@ function subjectLockDir(artifactRoot: string, subjectId: string): string {
|
|
|
254
270
|
* the write) of all three local-file mutators in this, since all three mutate the same record
|
|
255
271
|
* file for a given subject.
|
|
256
272
|
*/
|
|
257
|
-
function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () => T): T {
|
|
273
|
+
export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () => T): T {
|
|
258
274
|
const lockDir = subjectLockDir(artifactRoot, subjectId);
|
|
259
275
|
const staleMs = Number(process.env.FLOW_AGENTS_ASSIGNMENT_STALE_LOCK_MS ?? 5 * 60 * 1000);
|
|
260
276
|
const deadline = Date.now() + 30000;
|
|
@@ -305,7 +321,7 @@ function withSubjectLock<T>(artifactRoot: string, subjectId: string, body: () =>
|
|
|
305
321
|
* through here too means `--now` deterministically governs idle_days as well as liveness
|
|
306
322
|
* freshness, rather than idle_days silently reading the real wall clock regardless of `--now`.
|
|
307
323
|
*/
|
|
308
|
-
function computeEffectiveState(assignment: AssignmentStatus, freshHoldersList: FreshHolder[], selfActor: string | undefined, nowMs: number): {
|
|
324
|
+
export function computeEffectiveState(assignment: AssignmentStatus, freshHoldersList: FreshHolder[], selfActor: string | undefined, nowMs: number): {
|
|
309
325
|
effective_state: EffectiveState;
|
|
310
326
|
reason: string;
|
|
311
327
|
holder?: { actor?: string; assignee?: string | null; idle_days?: number | null; last_at?: string };
|
|
@@ -339,7 +355,14 @@ function computeEffectiveState(assignment: AssignmentStatus, freshHoldersList: F
|
|
|
339
355
|
return { effective_state: "human-held", reason: "assignee_without_claim_record", holder: { assignee: assignment.assignee } };
|
|
340
356
|
}
|
|
341
357
|
|
|
342
|
-
|
|
358
|
+
// F1 fix (fix-plan iteration 1, HIGH): prefer the canonical actor_key over re-serializing
|
|
359
|
+
// record.actor. record.actor_key IS resolveActor(env).actor for records written by the fixed
|
|
360
|
+
// performLocalClaim/performLocalSupersede paths (below) — the same flat/bare string liveness
|
|
361
|
+
// whoami, `liveness claim --actor`, per-actor current.json, and pull-work's --self-actor all
|
|
362
|
+
// use — so for an explicit-override actor the self-check and the liveness join now agree with
|
|
363
|
+
// every other tool. BACK-COMPAT: records with no actor_key (every pre-fix record, every #290
|
|
364
|
+
// eval fixture) fall back to serializeActor(record.actor) exactly as before this fix.
|
|
365
|
+
const holderActorKey = record.actor_key || loadActorIdentityHelper().serializeActor(record.actor);
|
|
343
366
|
if (selfActor && holderActorKey === selfActor) return { effective_state: "held", reason: "self_is_holder", holder: { actor: holderActorKey } };
|
|
344
367
|
|
|
345
368
|
const fresh = freshHoldersList.find((holder) => holder.actor === holderActorKey);
|
|
@@ -466,26 +489,27 @@ function loadLivenessInputs(args: ParsedArgs): { events: AnyObj[] | null; selfAc
|
|
|
466
489
|
// ─── local-file: claim | release | supersede (the durable-write path; real I/O by design — no
|
|
467
490
|
// external mutation to defer to a skill for this provider kind, per Design Decision 1) ─────────
|
|
468
491
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside claimLocalFile's
|
|
494
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's ownership
|
|
495
|
+
* guard (workflow-sidecar.ts, Wave 2) can reuse the EXACT same claim logic — same-actor idempotent
|
|
496
|
+
* refresh, different-actor throw, atomic write under withSubjectLock — rather than reimplementing
|
|
497
|
+
* a second, parallel claim path. claimLocalFile (CLI wrapper, below) is now a thin
|
|
498
|
+
* parse-args/print-envelope shell around this.
|
|
499
|
+
*/
|
|
500
|
+
export function performLocalClaim(
|
|
501
|
+
artifactRoot: string,
|
|
502
|
+
subjectId: string,
|
|
503
|
+
actor: ActorStruct,
|
|
504
|
+
opts: { ttlSeconds: number; branch: string; artifactDir: string; reason?: string; actorKey?: string },
|
|
505
|
+
): AssignmentClaimRecord {
|
|
476
506
|
const helper = loadActorIdentityHelper();
|
|
477
|
-
|
|
478
|
-
const ttlSecondsRaw = flagString(args.flags, "ttl-seconds", "1800") ?? "1800";
|
|
479
|
-
const ttlSeconds = Number(ttlSecondsRaw);
|
|
480
|
-
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
481
|
-
const branch = requireFlag(args, "branch");
|
|
482
|
-
const artifactDir = requireFlag(args, "artifact-dir");
|
|
483
|
-
const reason = flagString(args.flags, "reason") ?? "claim";
|
|
507
|
+
const reason = opts.reason ?? "claim";
|
|
484
508
|
|
|
485
509
|
// F1 fix (fix-plan iteration 1, CRITICAL): the existing-claim check AND the write must happen
|
|
486
510
|
// atomically with respect to any other `assignment-provider` invocation on the same subject —
|
|
487
511
|
// see withSubjectLock()'s doc comment for the full rationale.
|
|
488
|
-
return withSubjectLock(artifactRoot, subjectId, ():
|
|
512
|
+
return withSubjectLock(artifactRoot, subjectId, (): AssignmentClaimRecord => {
|
|
489
513
|
const existing = readLocalRecord(artifactRoot, subjectId);
|
|
490
514
|
if (existing && existing.status === "claimed") {
|
|
491
515
|
const existingActorKey = helper.serializeActor(existing.actor);
|
|
@@ -502,19 +526,39 @@ function claimLocalFile(argv: string[]): number {
|
|
|
502
526
|
role: "AssignmentClaimRecord",
|
|
503
527
|
subject_id: subjectId,
|
|
504
528
|
actor,
|
|
529
|
+
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
505
530
|
claimed_at: isoNow(),
|
|
506
|
-
ttl_seconds: ttlSeconds,
|
|
507
|
-
branch,
|
|
508
|
-
artifact_dir: artifactDir,
|
|
531
|
+
ttl_seconds: opts.ttlSeconds,
|
|
532
|
+
branch: opts.branch,
|
|
533
|
+
artifact_dir: opts.artifactDir,
|
|
509
534
|
status: "claimed",
|
|
510
535
|
audit_trail: [...(existing?.audit_trail ?? []), { at: isoNow(), transition: "claim", from_actor: null, to_actor: actor, reason }],
|
|
511
536
|
};
|
|
512
537
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
513
|
-
|
|
514
|
-
return 0;
|
|
538
|
+
return record;
|
|
515
539
|
});
|
|
516
540
|
}
|
|
517
541
|
|
|
542
|
+
function claimLocalFile(argv: string[]): number {
|
|
543
|
+
const args = parseArgs(argv);
|
|
544
|
+
const provider = flagString(args.flags, "provider", "local-file");
|
|
545
|
+
if (provider !== "local-file") throw new Error(`claim: --provider must be local-file (use render-claim for github); got ${provider}`);
|
|
546
|
+
const artifactRoot = requireFlag(args, "artifact-root");
|
|
547
|
+
const subjectId = requireFlag(args, "subject-id");
|
|
548
|
+
const { actor, actorKey } = loadActorStruct(args);
|
|
549
|
+
|
|
550
|
+
const ttlSecondsRaw = flagString(args.flags, "ttl-seconds", "1800") ?? "1800";
|
|
551
|
+
const ttlSeconds = Number(ttlSecondsRaw);
|
|
552
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
553
|
+
const branch = requireFlag(args, "branch");
|
|
554
|
+
const artifactDir = requireFlag(args, "artifact-dir");
|
|
555
|
+
const reason = flagString(args.flags, "reason") ?? "claim";
|
|
556
|
+
|
|
557
|
+
const record = performLocalClaim(artifactRoot, subjectId, actor, { ttlSeconds, branch, artifactDir, reason, actorKey });
|
|
558
|
+
console.log(JSON.stringify({ role: "AssignmentClaimResult", subject_id: subjectId, record }, null, 2));
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
518
562
|
function releaseLocalFile(argv: string[]): number {
|
|
519
563
|
const args = parseArgs(argv);
|
|
520
564
|
const provider = flagString(args.flags, "provider", "local-file");
|
|
@@ -540,23 +584,28 @@ function releaseLocalFile(argv: string[]): number {
|
|
|
540
584
|
});
|
|
541
585
|
}
|
|
542
586
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
|
|
589
|
+
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
|
|
590
|
+
* `--supersede-stale` takeover path (workflow-sidecar.ts, Wave 2) can reuse the EXACT same
|
|
591
|
+
* supersede logic — from-actor holder verification, ttl/branch/artifact_dir carry-forward,
|
|
592
|
+
* audit-trail append, atomic write under withSubjectLock — rather than reimplementing a second,
|
|
593
|
+
* parallel supersede path. supersedeLocalFile (CLI wrapper, below) is now a thin
|
|
594
|
+
* parse-args/print-envelope shell around this.
|
|
595
|
+
*/
|
|
596
|
+
export function performLocalSupersede(
|
|
597
|
+
artifactRoot: string,
|
|
598
|
+
subjectId: string,
|
|
599
|
+
fromActor: ActorStruct,
|
|
600
|
+
toActor: ActorStruct,
|
|
601
|
+
opts: { ttlSeconds?: number; branch?: string; artifactDir?: string; reason?: string; actorKey?: string } = {},
|
|
602
|
+
): AssignmentClaimRecord {
|
|
549
603
|
const helper = loadActorIdentityHelper();
|
|
550
|
-
const
|
|
551
|
-
const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
|
|
552
|
-
const reason = flagString(args.flags, "reason") ?? "supersede";
|
|
553
|
-
const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
|
|
554
|
-
const branchOverride = flagString(args.flags, "branch");
|
|
555
|
-
const artifactDirOverride = flagString(args.flags, "artifact-dir");
|
|
604
|
+
const reason = opts.reason ?? "supersede";
|
|
556
605
|
|
|
557
606
|
// F1 fix (fix-plan iteration 1, CRITICAL): supersede mutates the same record file claim/release
|
|
558
607
|
// do, under the same per-subject lock (see withSubjectLock()'s doc comment).
|
|
559
|
-
return withSubjectLock(artifactRoot, subjectId, ():
|
|
608
|
+
return withSubjectLock(artifactRoot, subjectId, (): AssignmentClaimRecord => {
|
|
560
609
|
const existing = readLocalRecord(artifactRoot, subjectId);
|
|
561
610
|
if (!existing || existing.status !== "claimed") throw new Error(`no active claim to supersede for subject: ${subjectId}`);
|
|
562
611
|
|
|
@@ -564,7 +613,7 @@ function supersedeLocalFile(argv: string[]): number {
|
|
|
564
613
|
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`);
|
|
565
614
|
}
|
|
566
615
|
|
|
567
|
-
const ttlSecondsRaw =
|
|
616
|
+
const ttlSecondsRaw = opts.ttlSeconds != null ? String(opts.ttlSeconds) : String(existing.ttl_seconds);
|
|
568
617
|
const ttlSeconds = Number(ttlSecondsRaw);
|
|
569
618
|
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw new Error(`--ttl-seconds must be a positive number; got ${ttlSecondsRaw}`);
|
|
570
619
|
|
|
@@ -573,19 +622,42 @@ function supersedeLocalFile(argv: string[]): number {
|
|
|
573
622
|
role: "AssignmentClaimRecord",
|
|
574
623
|
subject_id: subjectId,
|
|
575
624
|
actor: toActor,
|
|
625
|
+
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
576
626
|
claimed_at: isoNow(),
|
|
577
627
|
ttl_seconds: ttlSeconds,
|
|
578
|
-
branch:
|
|
579
|
-
artifact_dir:
|
|
628
|
+
branch: opts.branch ?? existing.branch,
|
|
629
|
+
artifact_dir: opts.artifactDir ?? existing.artifact_dir,
|
|
580
630
|
status: "claimed",
|
|
581
631
|
audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "supersede", from_actor: fromActor, to_actor: toActor, reason }],
|
|
582
632
|
};
|
|
583
633
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
584
|
-
|
|
585
|
-
return 0;
|
|
634
|
+
return record;
|
|
586
635
|
});
|
|
587
636
|
}
|
|
588
637
|
|
|
638
|
+
function supersedeLocalFile(argv: string[]): number {
|
|
639
|
+
const args = parseArgs(argv);
|
|
640
|
+
const provider = flagString(args.flags, "provider", "local-file");
|
|
641
|
+
if (provider !== "local-file") throw new Error(`supersede: --provider must be local-file (use render-supersede for github); got ${provider}`);
|
|
642
|
+
const artifactRoot = requireFlag(args, "artifact-root");
|
|
643
|
+
const subjectId = requireFlag(args, "subject-id");
|
|
644
|
+
const fromActor = loadActorStructFromFile(requireFlag(args, "from-actor-json"));
|
|
645
|
+
const toActor = loadActorStructFromFile(requireFlag(args, "to-actor-json"));
|
|
646
|
+
const reason = flagString(args.flags, "reason") ?? "supersede";
|
|
647
|
+
const ttlSecondsOverride = flagString(args.flags, "ttl-seconds");
|
|
648
|
+
const branchOverride = flagString(args.flags, "branch");
|
|
649
|
+
const artifactDirOverride = flagString(args.flags, "artifact-dir");
|
|
650
|
+
|
|
651
|
+
const record = performLocalSupersede(artifactRoot, subjectId, fromActor, toActor, {
|
|
652
|
+
ttlSeconds: ttlSecondsOverride != null ? Number(ttlSecondsOverride) : undefined,
|
|
653
|
+
branch: branchOverride ?? undefined,
|
|
654
|
+
artifactDir: artifactDirOverride ?? undefined,
|
|
655
|
+
reason,
|
|
656
|
+
});
|
|
657
|
+
console.log(JSON.stringify({ role: "AssignmentSupersedeResult", subject_id: subjectId, record }, null, 2));
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
589
661
|
// ─── GitHub: render-claim | render-release | render-supersede (render, don't execute — Design
|
|
590
662
|
// Decision 1). Pure functions: no I/O beyond reading --input-json/--actor-json. Never invoke
|
|
591
663
|
// `gh` (or any process) here — the calling skill runs the emitted argv verbatim. ────────────────
|
|
@@ -745,6 +817,18 @@ function renderSupersede(argv: string[]): number {
|
|
|
745
817
|
|
|
746
818
|
// ─── status | list (both provider kinds) ────────────────────────────────────────────────────
|
|
747
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
|
|
822
|
+
* mirrored exactly so ensure-session's ownership guard (workflow-sidecar.ts, Wave 2) derives an
|
|
823
|
+
* AssignmentStatus identically to the `assignment-provider status` CLI command — a single
|
|
824
|
+
* implementation, not a second parallel local-file read.
|
|
825
|
+
*/
|
|
826
|
+
export function readLocalAssignmentStatus(artifactRoot: string, subjectId: string): AssignmentStatus {
|
|
827
|
+
const record = readLocalRecord(artifactRoot, subjectId);
|
|
828
|
+
const active = record && record.status === "claimed" ? record : null;
|
|
829
|
+
return { subject_id: subjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
|
|
830
|
+
}
|
|
831
|
+
|
|
748
832
|
function statusCommand(argv: string[]): number {
|
|
749
833
|
const args = parseArgs(argv);
|
|
750
834
|
const provider = requireFlag(args, "provider");
|
|
@@ -754,9 +838,7 @@ function statusCommand(argv: string[]): number {
|
|
|
754
838
|
if (provider === "local-file") {
|
|
755
839
|
const artifactRoot = requireFlag(args, "artifact-root");
|
|
756
840
|
if (!requestedSubjectId) throw new Error("--subject-id is required for status --provider local-file");
|
|
757
|
-
|
|
758
|
-
const active = record && record.status === "claimed" ? record : null;
|
|
759
|
-
assignment = { subject_id: requestedSubjectId, provider: "local-file", assignee: active ? loadActorIdentityHelper().serializeActor(active.actor) : null, record: active };
|
|
841
|
+
assignment = readLocalAssignmentStatus(artifactRoot, requestedSubjectId);
|
|
760
842
|
} else if (provider === "github") {
|
|
761
843
|
const issueJsonPath = requireFlag(args, "issue-json");
|
|
762
844
|
const issue = loadJsonInput(issueJsonPath) as GithubIssueDoc;
|