@kontourai/flow-agents 3.2.0 → 3.3.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 +12 -0
- package/CHANGELOG.md +13 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +224 -0
- package/build/src/cli/workflow-sidecar.js +775 -4
- package/build/src/tools/validate-source-tree.js +3 -2
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/workflow-steering.js +73 -1
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/index.md +2 -2
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/fixture-ownership.md +3 -2
- package/docs/index.md +4 -0
- package/docs/integrations/flow-agents-console.md +108 -0
- package/docs/integrations/index.md +4 -0
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +59 -2
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +91 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/run.sh +8 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +73 -0
- package/kits/builder/skills/pull-work/SKILL.md +12 -2
- package/kits/veritas-governance/docs/README.md +81 -3
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +5 -0
- package/package.json +1 -1
- package/scripts/ci/trust-reconcile.js +78 -253
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/workflow-steering.js +73 -1
- package/scripts/lib/reconcile-shape.js +381 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +866 -4
- package/src/tools/validate-source-tree.ts +3 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type EffectiveState } from "./assignment-provider.js";
|
|
2
3
|
type AnyObj = Record<string, any>;
|
|
3
4
|
export declare const statuses: Set<string>;
|
|
4
5
|
export declare const phases: string[];
|
|
@@ -201,6 +202,207 @@ export declare function normalizeFinding(raw: AnyObj): AnyObj;
|
|
|
201
202
|
* Signing is ALWAYS fail-open — a signing failure never breaks the seal.
|
|
202
203
|
*/
|
|
203
204
|
export declare function sealTrustCheckpoint(dir: string, slug: string, sealedAt: string, status: string, phase: string): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Distinct, identifiable error for a shape-invalid trust.bundle — NOT a generic Error, so
|
|
207
|
+
* every publishDelivery() call site (advanceState/recordRelease/promote/publishDeliveryCmd)
|
|
208
|
+
* can positively distinguish "refuses to publish an invalid bundle" (must be LOUD, fail
|
|
209
|
+
* closed, AC6) from the other failure modes those call sites already tolerate as best-effort
|
|
210
|
+
* (missing kits/ ancestor for a scratch dir, I/O errors, etc — see each catch's own comment).
|
|
211
|
+
* A bare `instanceof Error` check would not suffice since every thrown failure in this file is
|
|
212
|
+
* already an Error; `code` is the recognizable, grep-stable discriminator.
|
|
213
|
+
*/
|
|
214
|
+
export declare class InvalidBundleShapeError extends Error {
|
|
215
|
+
readonly code: "RECONCILE_PREFLIGHT_INVALID_SHAPE";
|
|
216
|
+
readonly issues: string[];
|
|
217
|
+
constructor(issues: string[]);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Distinct, identifiable error for a publish attempt by an actor who is NOT the fresh,
|
|
221
|
+
* non-superseded holder of the subject (issue #293, ADR 0021 §3) — NOT a generic Error and NOT
|
|
222
|
+
* `InvalidBundleShapeError` (#356), so every publishDelivery() call site can positively
|
|
223
|
+
* distinguish "refuses to publish because the bundle shape is invalid" from "refuses to publish
|
|
224
|
+
* because this actor no longer holds the subject" — two genuinely distinct fail-closed tiers
|
|
225
|
+
* that must never be conflated in a catch handler (a worker fixing a shape issue must not be
|
|
226
|
+
* told to "re-record evidence" when the real problem is a stale/superseded claim, and vice
|
|
227
|
+
* versa). Mirrors `InvalidBundleShapeError`'s shape exactly: same `extends Error` base, same
|
|
228
|
+
* `readonly code` discriminator convention, same doc-comment structure — only the payload
|
|
229
|
+
* differs (the effective-state/holder/reason/guidance `runVerifyHold()` computed, rather than a
|
|
230
|
+
* shape-issue list).
|
|
231
|
+
*/
|
|
232
|
+
export declare class NotFreshHolderError extends Error {
|
|
233
|
+
readonly code: "VERIFY_HOLD_NOT_FRESH_HOLDER";
|
|
234
|
+
readonly effective_state: EffectiveState | "not_evaluated";
|
|
235
|
+
readonly holder?: {
|
|
236
|
+
actor?: string;
|
|
237
|
+
assignee?: string | null;
|
|
238
|
+
idle_days?: number | null;
|
|
239
|
+
last_at?: string;
|
|
240
|
+
};
|
|
241
|
+
readonly reason: string;
|
|
242
|
+
readonly guidance: string[];
|
|
243
|
+
constructor(result: {
|
|
244
|
+
effective_state: EffectiveState | "not_evaluated";
|
|
245
|
+
holder?: {
|
|
246
|
+
actor?: string;
|
|
247
|
+
assignee?: string | null;
|
|
248
|
+
idle_days?: number | null;
|
|
249
|
+
last_at?: string;
|
|
250
|
+
};
|
|
251
|
+
reason: string;
|
|
252
|
+
guidance: string[];
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export declare function runReconcilePreflight(dir: string, repoRoot: string, manifestOverride?: string | null): {
|
|
256
|
+
ok: boolean;
|
|
257
|
+
issues: string[];
|
|
258
|
+
warnings: string[];
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The ONE hard-stop gate (issue #293, ADR 0021 §3): "is my actor the fresh, non-superseded
|
|
262
|
+
* holder of this subject (or is the subject free/self-held)?" Reuses the SAME assignment ⋈
|
|
263
|
+
* liveness join #290/#291 already built (`computeEffectiveState` + `readLocalAssignmentStatus`
|
|
264
|
+
* + `readLivenessEvents`/`freshHolders`) — no second computation is invented here. This is the
|
|
265
|
+
* mirror-image check, at the OTHER end of the session lifecycle, of
|
|
266
|
+
* `enforceEnsureSessionOwnership`'s pre-entry guard above: that guard decides whether entry
|
|
267
|
+
* should claim/supersede/refuse; this gate decides whether PUBLISH should proceed, with a
|
|
268
|
+
* different (new) per-effective-state mapping — the ownership guard's "claim"/"supersede"
|
|
269
|
+
* actions are wrong at publish time, so this is new interpretation of the existing join output,
|
|
270
|
+
* not a new join and not a new `EffectiveState` value (there is no literal `"superseded"` state
|
|
271
|
+
* — a superseded-away actor's own re-check naturally resolves to `held`(holder=successor) or
|
|
272
|
+
* `reclaimable`, never `self_is_holder`; see assignment-provider.ts's `computeEffectiveState`).
|
|
273
|
+
*
|
|
274
|
+
* Effective-state -> publish-decision mapping (the operative spec, from the plan's table).
|
|
275
|
+
* IMPORTANT (bug fix, #397): this gate fences the durable ASSIGNMENT hold, not ambient
|
|
276
|
+
* liveness — liveness is advisory everywhere else in the system, and this is the ONE hard
|
|
277
|
+
* gate, so it must never false-block on liveness alone. A subject with fresh liveness by
|
|
278
|
+
* another actor but NO durable assignment record (`held`, reason
|
|
279
|
+
* `liveness_claim_present_assignment_lagging`) therefore PASSES, not BLOCKs — there is no
|
|
280
|
+
* durable ownership conflict to protect, only ambient presence. This does NOT weaken zombie
|
|
281
|
+
* protection: a superseded zombie always has an assignment record (either its own stale
|
|
282
|
+
* assignment — `reclaimable` — or the successor's assignment-backed `held`/
|
|
283
|
+
* `fresh_liveness_heartbeat`), both of which still BLOCK below.
|
|
284
|
+
* - `free` (no assignment ever recorded) -> PASS (never false-block an untracked subject)
|
|
285
|
+
* - `held`, reason `self_is_holder` -> PASS (the caller IS the current holder)
|
|
286
|
+
* - `held`, reason `liveness_claim_present_assignment_lagging`
|
|
287
|
+
* (fresh liveness by another actor, NO durable assignment record) -> PASS (liveness alone
|
|
288
|
+
* is never a durable ownership conflict — see #397 fix note above)
|
|
289
|
+
* - `held`, reason `fresh_liveness_heartbeat`
|
|
290
|
+
* (durable assignment held by another actor + fresh liveness) -> BLOCK (a different
|
|
291
|
+
* actor durably holds this subject and is demonstrably still live)
|
|
292
|
+
* - `reclaimable` (assignment present, stale/absent liveness) -> BLOCK (not proof of
|
|
293
|
+
* self — safe default blocks; a durable assignment record exists)
|
|
294
|
+
* - `human-held` -> BLOCK (never publish over a human assignee)
|
|
295
|
+
* - no resolvable join at all (non-local-file provider
|
|
296
|
+
* with no --effective-state-json) -> `not_evaluated`, PASS (documented scope
|
|
297
|
+
* boundary, loud stderr note, never a silent block)
|
|
298
|
+
*
|
|
299
|
+
* Actor resolution mirrors `enforceEnsureSessionOwnership` EXACTLY: the bare
|
|
300
|
+
* `resolveActor(env).actor` / branchActorKey string, NEVER `serializeActor(actorStruct)`'s
|
|
301
|
+
* triple — the #291 flat-vs-triple seam. `opts.actorKey` (the CLI's --actor / FLOW_AGENTS_ACTOR
|
|
302
|
+
* override, already sanitized by the caller) takes precedence when provided.
|
|
303
|
+
*
|
|
304
|
+
* Slug/artifactRoot derivation matches `publishDelivery()`'s own `slug =
|
|
305
|
+
* path.basename(path.resolve(dir))` byte-for-byte (both must resolve the SAME assignment
|
|
306
|
+
* record) and the `artifactRoot = path.dirname(dir)` pattern already used elsewhere in this
|
|
307
|
+
* file for a `dir`-only consumer.
|
|
308
|
+
*
|
|
309
|
+
* github provider: read-only precomputed `--effective-state-json` escape hatch, matching
|
|
310
|
+
* `enforceEnsureSessionOwnership`'s existing github branch precedent exactly (render-don't-
|
|
311
|
+
* execute — assignment-provider.ts never calls `gh` directly from this CLI).
|
|
312
|
+
*
|
|
313
|
+
* Every interpolated actor/holder/reason string is sanitized via
|
|
314
|
+
* `stripControlCharsForDisplay(...).slice(0, 64)` — reusing the exact top-level helper, never a
|
|
315
|
+
* second sanitizer (AC7).
|
|
316
|
+
*
|
|
317
|
+
* SECOND CI-blocking false-block fix (this iteration): the hard gate above enforces ONLY for a
|
|
318
|
+
* STABLY-identified actor. A caller's identity is STABLE when it comes from an explicit
|
|
319
|
+
* `opts.actorKey` (a caller that hands in an actor key is asserting a stable identity — e.g. the
|
|
320
|
+
* CLI's --actor / FLOW_AGENTS_ACTOR override, or the github skill's precomputed path), from
|
|
321
|
+
* `FLOW_AGENTS_ACTOR` (`resolveActor` source `"explicit-override"`), or from a runtime-native
|
|
322
|
+
* session id (`resolveActor` source `` `runtime-session-id:${runtime}` ``, e.g. Claude Code's
|
|
323
|
+
* `CLAUDE_CODE_SESSION_ID`). It is UNSTABLE when `resolveActor` falls all the way through to the
|
|
324
|
+
* process-ancestry fallback (source `"process-ancestry"`) or fails to resolve at all (source
|
|
325
|
+
* `"unresolved"` / `isUnresolvedActor(actor)`). This mirrors `enforceEnsureSessionOwnership`'s
|
|
326
|
+
* Design Decision 4 (never hard-fail on actor ambiguity): a session that cannot stably
|
|
327
|
+
* self-identify cannot be meaningfully fenced against a durable assignment record, so hard-
|
|
328
|
+
* blocking it produces exactly the false-block CI hit (no `FLOW_AGENTS_ACTOR`/runtime session id
|
|
329
|
+
* in CI -> ancestry fallback -> a DIFFERENT ancestry-derived actor string than the one that
|
|
330
|
+
* created the claim -> `reclaimable`/not-self -> hard BLOCK of a legitimate self-publish). A
|
|
331
|
+
* real coordination participant always has a stable identity (explicit override or runtime
|
|
332
|
+
* session id), so zombie protection under a stable actor is completely unaffected — this only
|
|
333
|
+
* changes behavior for a caller that was never fenceable in the first place. When the resolved
|
|
334
|
+
* actor identity is UNSTABLE, this function short-circuits to `{ ok: true, effective_state:
|
|
335
|
+
* "not_evaluated", reason: "actor-identity-unstable-advisory-only" }` (holder included, sanitized,
|
|
336
|
+
* when one exists) BEFORE running the assignment ⋈ liveness join at all, with one loud stderr
|
|
337
|
+
* note — the gate degrades to advisory-only rather than ever false-blocking an unstable identity.
|
|
338
|
+
* When the actor identity IS stable, the effective-state -> publish-decision mapping above
|
|
339
|
+
* applies exactly as documented, unchanged.
|
|
340
|
+
*/
|
|
341
|
+
export declare function runVerifyHold(dir: string, repoRoot: string | null, opts?: {
|
|
342
|
+
actorKey?: string;
|
|
343
|
+
now?: number;
|
|
344
|
+
assignmentProviderKind?: string;
|
|
345
|
+
effectiveStateJson?: unknown;
|
|
346
|
+
}): {
|
|
347
|
+
ok: boolean;
|
|
348
|
+
effective_state: EffectiveState | "not_evaluated";
|
|
349
|
+
holder?: {
|
|
350
|
+
actor?: string;
|
|
351
|
+
assignee?: string | null;
|
|
352
|
+
idle_days?: number | null;
|
|
353
|
+
last_at?: string;
|
|
354
|
+
};
|
|
355
|
+
reason: string;
|
|
356
|
+
guidance: string[];
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* Render (never execute) the takeover protocol for a candidate the skill has selected. Takeover is
|
|
360
|
+
* RESUMPTION, not restart: when a subject is `reclaimable` (a durable assignment whose incumbent is
|
|
361
|
+
* no longer heartbeating), a successor must (1) grace-beat — wait one heartbeat interval and re-check,
|
|
362
|
+
* backing off if the incumbent revives — (2) supersede via `ensure-session --supersede-stale`, and
|
|
363
|
+
* (3) CONTINUE THE INCUMBENT'S BRANCH (from the incumbent's claim record), never a parallel one, and
|
|
364
|
+
* re-enter the SAME artifact dir (deterministic slug). This function computes the effective state
|
|
365
|
+
* (the same assignment ⋈ liveness join verify-hold/ensure-session use) and emits the appropriate
|
|
366
|
+
* action + the incumbent's `resume_branch`; the skill executes the beat/supersede/git steps.
|
|
367
|
+
*
|
|
368
|
+
* Race-safety is CLI-enforced at supersede time, not here: `ensure-session --supersede-stale`
|
|
369
|
+
* re-computes the state and refuses (dies on `held`) if the incumbent revived during the beat, so a
|
|
370
|
+
* successor that skipped or lost the grace race still cannot supersede a live incumbent (AC2a).
|
|
371
|
+
*
|
|
372
|
+
* ADVISORY, not a gate: the assignment ⋈ liveness JOIN is identical to verify-hold's, but this render
|
|
373
|
+
* only advises the skill which action to take — the ACTUAL enforcement is `ensure-session
|
|
374
|
+
* --supersede-stale` (re-check under a subject lock). So it deliberately does NOT replicate
|
|
375
|
+
* verify-hold's #398 `isStableActor` tiering (which exists there only because verify-hold is a hard
|
|
376
|
+
* publish BLOCK that must not false-block an unstable/ancestry identity): a wrong self-identification
|
|
377
|
+
* here yields only a wrong advisory string, never a wrong mutation.
|
|
378
|
+
*
|
|
379
|
+
* Ordering note: ADR 0021 §5's literal wording is supersede→grace→maybe-undo; this implements
|
|
380
|
+
* grace→recheck→supersede (never write until the beat confirms the incumbent stayed stale) — a
|
|
381
|
+
* deliberate, safer variant that preserves the ADR's intent ("laptop just woke resolves in the
|
|
382
|
+
* incumbent's favor") without a premature write to walk back.
|
|
383
|
+
*
|
|
384
|
+
* All untrusted fields (incumbent actor / branch / last_at, sourced from the shared multi-writer
|
|
385
|
+
* assignment+liveness stream) pass through `sanitize`/`sanitizeWide` (stripControlCharsForDisplay +
|
|
386
|
+
* the repo's 64/240 cap tiers — branch uses the 240 free-text tier so a valid long branch is never
|
|
387
|
+
* truncated into a bad checkout target) — the #287/#320/#293 injection class.
|
|
388
|
+
*/
|
|
389
|
+
export declare function runTakeoverPreflight(dir: string, opts?: {
|
|
390
|
+
actorKey?: string;
|
|
391
|
+
now?: number;
|
|
392
|
+
graceSeconds?: number;
|
|
393
|
+
}): {
|
|
394
|
+
ok: boolean;
|
|
395
|
+
action: "grace-then-supersede" | "back-off" | "claim" | "ask-first" | "proceed";
|
|
396
|
+
effective_state: EffectiveState;
|
|
397
|
+
reason: string;
|
|
398
|
+
holder?: {
|
|
399
|
+
actor?: string;
|
|
400
|
+
last_at?: string;
|
|
401
|
+
};
|
|
402
|
+
resume_branch?: string;
|
|
403
|
+
grace_seconds?: number;
|
|
404
|
+
next_steps: string[];
|
|
405
|
+
};
|
|
204
406
|
/**
|
|
205
407
|
* Publish the session's trust artifacts to the committed delivery/ path.
|
|
206
408
|
*
|
|
@@ -226,6 +428,28 @@ export declare function sealTrustCheckpoint(dir: string, slug: string, sealedAt:
|
|
|
226
428
|
* evals/integration/test_checkpoint_signing.sh TEST 2 and the WS5 session findings at
|
|
227
429
|
* .kontourai/flow-agents/ws5-governance-kit-slice1 for the root cause this fixes).
|
|
228
430
|
* Idempotent: overwrites on re-delivery to the same slug.
|
|
431
|
+
*
|
|
432
|
+
* #356 Wave 3 (AC6): Fail-CLOSED on bundle SHAPE. After the fail-soft absence guard below,
|
|
433
|
+
* runs the SAME reconcile-preflight shape check (runReconcilePreflight) the
|
|
434
|
+
* `reconcile-preflight` CLI subcommand exposes, BEFORE copying anything into delivery/. A
|
|
435
|
+
* shape-invalid bundle throws InvalidBundleShapeError (a distinct, identifiable error — see
|
|
436
|
+
* its own doc comment) rather than silently publishing a bundle CI's Trust Reconcile job
|
|
437
|
+
* would reject anyway. This is intentionally a NEW, additive fail-closed branch — it must
|
|
438
|
+
* never be conflated with the pre-existing fail-soft absence/repo-root branches above/below,
|
|
439
|
+
* which stay exactly as before (see each publishDelivery call site's catch handler for how
|
|
440
|
+
* the distinction is preserved end-to-end).
|
|
441
|
+
*
|
|
442
|
+
* #293 (SECOND, DISTINCT fail-closed gate): immediately after the shape check above and BEFORE
|
|
443
|
+
* writing anything into delivery/, runs `runVerifyHold()` — the assignment ⋈ liveness join
|
|
444
|
+
* asking "is the calling actor the fresh, non-superseded holder of this subject (or is it
|
|
445
|
+
* free/self-held)?" A not-fresh-holder result throws `NotFreshHolderError` (distinct from
|
|
446
|
+
* `InvalidBundleShapeError` — a different `code`, a different failure mode: actor hold vs
|
|
447
|
+
* bundle shape). There are now THREE tiers here, and they must never be conflated in prose or
|
|
448
|
+
* in a call site's catch handler: (1) fail-SOFT bundle absence / repo-root resolution (silent
|
|
449
|
+
* no-op / visible warning, unchanged from before #356), (2) fail-CLOSED bundle shape (#356,
|
|
450
|
+
* `InvalidBundleShapeError`), (3) fail-CLOSED verify-hold (#293, `NotFreshHolderError`). The
|
|
451
|
+
* shape check runs first (per the plan's ordering) — a bundle that is BOTH shape-invalid and
|
|
452
|
+
* not-held throws `InvalidBundleShapeError` specifically, never `NotFreshHolderError`.
|
|
229
453
|
*/
|
|
230
454
|
export declare function publishDelivery(dir: string, repoRoot: string | null): Promise<void>;
|
|
231
455
|
export declare function validateLearningCorrection(record: AnyObj): void;
|