@kontourai/flow-agents 3.0.0 → 3.2.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 +32 -0
- package/CONTEXT.md +67 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +188 -0
- package/build/src/cli/assignment-provider.js +193 -51
- package/build/src/cli/workflow-sidecar.d.ts +16 -6
- package/build/src/cli/workflow-sidecar.js +492 -45
- 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 +10 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/scripts/hooks/config-protection.js +24 -4
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +262 -5
- 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 +113 -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 +6 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- 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 +46 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
- package/evals/run.sh +10 -0
- package/evals/static/test_knowledge_providers.sh +13 -4
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +69 -11
- package/kits/builder/skills/design-probe/SKILL.md +47 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +10 -0
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +147 -18
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
- package/kits/knowledge/adapters/shared/codec.js +265 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +517 -7
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -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/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/README.md +1 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +24 -4
- 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 +262 -5
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +239 -59
- package/src/cli/workflow-sidecar.ts +529 -43
- package/src/lib/flow-resolver.ts +35 -14
- package/src/tools/validate-source-tree.ts +2 -1
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
'use strict';
|
|
26
26
|
|
|
27
27
|
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
28
29
|
const path = require('path');
|
|
29
30
|
const { spawnSync } = require('child_process');
|
|
30
31
|
const crypto = require('crypto');
|
|
@@ -42,6 +43,8 @@ const {
|
|
|
42
43
|
flowAgentsArtifactRoot,
|
|
43
44
|
flowAgentsArtifactRootsForRead,
|
|
44
45
|
} = require('./lib/local-artifact-paths');
|
|
46
|
+
const { resolveActor, isUnresolvedActor, detectRuntime } = require('./lib/actor-identity.js');
|
|
47
|
+
const { readCurrentPointer } = require('./lib/current-pointer.js');
|
|
45
48
|
|
|
46
49
|
const MAX_STDIN = 1024 * 1024;
|
|
47
50
|
const ACTIVE_STATUSES = new Set([
|
|
@@ -1531,7 +1534,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
|
|
|
1531
1534
|
* to scanning all of .kontourai/flow-agents (newest-mtime).
|
|
1532
1535
|
*/
|
|
1533
1536
|
function preferredArtifactDir(flowAgentsDir) {
|
|
1534
|
-
const current =
|
|
1537
|
+
const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
|
|
1535
1538
|
if (!current) return null;
|
|
1536
1539
|
const slug = current.artifact_dir || current.active_slug;
|
|
1537
1540
|
if (typeof slug !== 'string' || !slug.trim()) return null;
|
|
@@ -1549,7 +1552,7 @@ function hasSidecarPresence(artifactDir) {
|
|
|
1549
1552
|
// return that slug so analyze() can log the staleness rather than silently falling back to
|
|
1550
1553
|
// a global mtime scan that could resurface an abandoned/never-real session as active.
|
|
1551
1554
|
function staleCurrentSlug(flowAgentsDir) {
|
|
1552
|
-
const current =
|
|
1555
|
+
const { payload: current } = readCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
|
|
1553
1556
|
if (!current) return null;
|
|
1554
1557
|
const slug = current.artifact_dir || current.active_slug;
|
|
1555
1558
|
if (typeof slug !== 'string' || !slug.trim()) return null;
|
|
@@ -1669,7 +1672,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1669
1672
|
artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
|
|
1670
1673
|
}
|
|
1671
1674
|
|
|
1672
|
-
if (artifacts.length === 0) return { warnings: [], blocking: false };
|
|
1675
|
+
if (artifacts.length === 0) return { warnings: [], blocking: false, latestArtifactDir: null };
|
|
1673
1676
|
|
|
1674
1677
|
const latest = artifacts[0];
|
|
1675
1678
|
const latestArtifactDir = path.dirname(latest.file);
|
|
@@ -1743,7 +1746,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1743
1746
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
1744
1747
|
return blockRe.test(w);
|
|
1745
1748
|
});
|
|
1746
|
-
return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep) };
|
|
1749
|
+
return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
1747
1750
|
}
|
|
1748
1751
|
|
|
1749
1752
|
/**
|
|
@@ -1842,12 +1845,266 @@ function remediationFor(warning) {
|
|
|
1842
1845
|
return null;
|
|
1843
1846
|
}
|
|
1844
1847
|
|
|
1848
|
+
// ─── #292 Wave 2: Stop-hook non-terminal release-with-handoff ────────────────
|
|
1849
|
+
//
|
|
1850
|
+
// When a session's Stop event fires and the session's state.json status is NOT
|
|
1851
|
+
// one of workflow-sidecar.ts's LIVENESS_TERMINAL statuses (delivered/accepted/
|
|
1852
|
+
// archived — the SAME set advance-state's terminal path already tests against,
|
|
1853
|
+
// imported here rather than re-declared, per AC8), the liveness claim and (for
|
|
1854
|
+
// the local-file assignment provider) the durable assignment claim would
|
|
1855
|
+
// otherwise sit held until TTL, even though the session has genuinely ended.
|
|
1856
|
+
// This closes that gap: always emit a provider-agnostic liveness release (AC1),
|
|
1857
|
+
// and for local-file additionally perform the real release inline (AC2), while
|
|
1858
|
+
// honestly disclosing (never executing) the equivalent GitHub-provider release
|
|
1859
|
+
// as a pending handoff.json intent (AC3). See the plan artifact
|
|
1860
|
+
// (.kontourai/flow-agents/kontourai-flow-agents-292/kontourai-flow-agents-292--plan-work.md,
|
|
1861
|
+
// "Wave 2") for the full design rationale and the render-don't-execute
|
|
1862
|
+
// constraint this deliberately does not cross.
|
|
1863
|
+
//
|
|
1864
|
+
// Fail-open (AC7): the ENTIRE body runs under one try/catch. Any failure —
|
|
1865
|
+
// missing build/, corrupt assignment record, unresolved actor, missing/
|
|
1866
|
+
// malformed provider settings — degrades to a single stderr diagnostic and
|
|
1867
|
+
// returns, never throwing, never affecting the Stop hook's own exit code or
|
|
1868
|
+
// goal-fit's warnings/blocking output (this function's return value is not
|
|
1869
|
+
// consumed by that contract at all — it is a pure side effect).
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* require() the compiled workflow-sidecar.js the same hasBuild/fs.existsSync-guarded
|
|
1873
|
+
* way loadActiveFlowStep() already requires flow-resolver.js. Returns null (never
|
|
1874
|
+
* throws) when build/ is absent or the require fails — the caller treats null as
|
|
1875
|
+
* "the LIVENESS_TERMINAL boundary is unknown; do not guess it" and skips the whole
|
|
1876
|
+
* release (fail-open, never a re-derived duplicate Set — AC8).
|
|
1877
|
+
*/
|
|
1878
|
+
function loadWorkflowSidecarBuilt() {
|
|
1879
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
1880
|
+
const builtSidecar = path.join(packageRoot, 'build', 'src', 'cli', 'workflow-sidecar.js');
|
|
1881
|
+
if (!fs.existsSync(builtSidecar)) return null; // hasBuild guard
|
|
1882
|
+
try {
|
|
1883
|
+
const mod = require(builtSidecar);
|
|
1884
|
+
if (!(mod.LIVENESS_TERMINAL instanceof Set)) return null;
|
|
1885
|
+
return mod;
|
|
1886
|
+
} catch {
|
|
1887
|
+
return null; // require failed — fail-open
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** Same hasBuild-guarded require idiom, for build/src/cli/assignment-provider.js. */
|
|
1892
|
+
function loadAssignmentProviderBuilt() {
|
|
1893
|
+
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
1894
|
+
const builtProvider = path.join(packageRoot, 'build', 'src', 'cli', 'assignment-provider.js');
|
|
1895
|
+
if (!fs.existsSync(builtProvider)) return null;
|
|
1896
|
+
try {
|
|
1897
|
+
const mod = require(builtProvider);
|
|
1898
|
+
if (typeof mod.performLocalRelease !== 'function') return null;
|
|
1899
|
+
return mod;
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Resolve the effective assignment-provider kind for this repo ('local-file' | 'github'),
|
|
1907
|
+
* or null when it cannot be determined. Prefers reading context/settings/
|
|
1908
|
+
* assignment-provider-settings.json directly (project settings first, matching
|
|
1909
|
+
* effective-assignment-provider-settings.ts's own lookup precedence) rather than shelling
|
|
1910
|
+
* out to or refactoring that CLI-shaped file (see plan Unresolved Question 1) — this task's
|
|
1911
|
+
* scope is the Stop hook, not that file. Returns null (never guesses) when no settings file
|
|
1912
|
+
* is present/parseable or names no provider kind for a project entry.
|
|
1913
|
+
*
|
|
1914
|
+
* Deliberately reads ONLY the project settings file (context/settings/
|
|
1915
|
+
* assignment-provider-settings.json relative to repo root) — the common case for this repo
|
|
1916
|
+
* and the one confirmed present. The effective() merge/repo-matching logic in
|
|
1917
|
+
* effective-assignment-provider-settings.ts is intentionally NOT reimplemented here (that
|
|
1918
|
+
* would fork a second merge algorithm); a repo whose provider kind can only be resolved via
|
|
1919
|
+
* global settings or multi-project merge degrades to "unknown, skip assignment-layer
|
|
1920
|
+
* release" rather than guessing.
|
|
1921
|
+
*/
|
|
1922
|
+
function resolveAssignmentProviderKind(root) {
|
|
1923
|
+
const settingsFile = path.join(root, 'context', 'settings', 'assignment-provider-settings.json');
|
|
1924
|
+
const settings = readJsonFile(settingsFile);
|
|
1925
|
+
if (!settings) return null;
|
|
1926
|
+
const candidates = [];
|
|
1927
|
+
if (settings.defaults && settings.defaults.provider) candidates.push(settings.defaults.provider);
|
|
1928
|
+
if (Array.isArray(settings.projects)) {
|
|
1929
|
+
for (const project of settings.projects) {
|
|
1930
|
+
if (project && project.provider) candidates.push(project.provider);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const kind = candidates.map(p => p && p.kind).find(k => k === 'local-file' || k === 'github');
|
|
1934
|
+
return kind || null;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Refresh handoff.json by merge (read+spread the existing file's JSON, overwrite only the
|
|
1939
|
+
* fields this task owns), mirroring advanceState's own handoff-write shape
|
|
1940
|
+
* (`{ ...loadJson(...), ...sidecarBase(slug), summary, current_state_ref: "state.json",
|
|
1941
|
+
* next_steps, blockers: [], warnings: [] }`) and current-pointer.js's writePerActorCurrent
|
|
1942
|
+
* write idiom (`fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n")`). next_steps
|
|
1943
|
+
* is appended to (not clobbered) when a provider-release next-step is due.
|
|
1944
|
+
*/
|
|
1945
|
+
function refreshHandoffAfterRelease(artifactDir, slug, state, providerReleasePending, providerReleaseNextCommand) {
|
|
1946
|
+
const file = path.join(artifactDir, 'handoff.json');
|
|
1947
|
+
const existing = readJsonFile(file) || {};
|
|
1948
|
+
const status = state ? normalizedStatus(state.status || 'unknown') : 'unknown';
|
|
1949
|
+
const phase = state ? normalizedStatus(state.phase || 'unknown') : 'unknown';
|
|
1950
|
+
const summary = (state && state.next_action && state.next_action.summary)
|
|
1951
|
+
? String(state.next_action.summary)
|
|
1952
|
+
: `session ended at status:${status} phase:${phase}`;
|
|
1953
|
+
const existingNextSteps = Array.isArray(existing.next_steps) ? existing.next_steps.slice() : [];
|
|
1954
|
+
const nextSteps = existingNextSteps.slice();
|
|
1955
|
+
if (providerReleasePending && providerReleaseNextCommand && !nextSteps.includes(providerReleaseNextCommand)) {
|
|
1956
|
+
nextSteps.push(providerReleaseNextCommand);
|
|
1957
|
+
}
|
|
1958
|
+
const payload = {
|
|
1959
|
+
...existing,
|
|
1960
|
+
schema_version: existing.schema_version || '1.0',
|
|
1961
|
+
task_slug: existing.task_slug || slug,
|
|
1962
|
+
summary,
|
|
1963
|
+
current_state_ref: 'state.json',
|
|
1964
|
+
next_steps: nextSteps,
|
|
1965
|
+
...(providerReleasePending ? { provider_release_pending: true } : {}),
|
|
1966
|
+
...(providerReleaseNextCommand ? { provider_release_next_command: providerReleaseNextCommand } : {}),
|
|
1967
|
+
};
|
|
1968
|
+
fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* #292 Wave 2: on a Stop event whose resolved session is at a NON-terminal status, release
|
|
1973
|
+
* the session's liveness claim (always, provider-agnostic — AC1) and, for the local-file
|
|
1974
|
+
* assignment provider, its durable assignment claim too (AC2), then refresh handoff.json
|
|
1975
|
+
* (AC4). A terminal-status session (already released by advance-state) is a deliberate no-op
|
|
1976
|
+
* (AC5). For the github provider, never calls render-release or gh — only records an
|
|
1977
|
+
* honestly-labeled pending intent in handoff.json (AC3). Actor-scoped: only ever releases
|
|
1978
|
+
* the CURRENT actor's own held claim (AC6, enforced inside performLocalRelease).
|
|
1979
|
+
*
|
|
1980
|
+
* Called once from run(), reusing analyze()'s already-resolved latestArtifactDir — this
|
|
1981
|
+
* function does not re-derive "what is the active session" a second time.
|
|
1982
|
+
*
|
|
1983
|
+
* Fail-open (AC7): the entire body is wrapped in one try/catch. Any failure degrades to a
|
|
1984
|
+
* single stderr diagnostic and returns, never throwing, never affecting the Stop hook's exit
|
|
1985
|
+
* code or goal-fit's warnings/blocking contract (this function's return value is unused by
|
|
1986
|
+
* that contract).
|
|
1987
|
+
*
|
|
1988
|
+
* @param {string} root - repo root (as resolved by findRepoRoot)
|
|
1989
|
+
* @param {string|null} artifactDir - analyze()'s already-resolved latestArtifactDir
|
|
1990
|
+
*/
|
|
1991
|
+
function releaseOnNonTerminalStop(root, artifactDir) {
|
|
1992
|
+
try {
|
|
1993
|
+
if (!artifactDir) return; // no active session resolved — nothing to release.
|
|
1994
|
+
|
|
1995
|
+
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
1996
|
+
if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
|
|
1997
|
+
|
|
1998
|
+
const sidecar = loadWorkflowSidecarBuilt();
|
|
1999
|
+
if (!sidecar) {
|
|
2000
|
+
process.stderr.write('[Hook] Goal Fit: stop-hook release skipped — build/src/cli/workflow-sidecar.js not available (LIVENESS_TERMINAL boundary unknown).\n');
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const status = normalizedStatus(state.status || 'unknown');
|
|
2005
|
+
if (sidecar.LIVENESS_TERMINAL.has(status)) return; // AC5: terminal — advance-state already released.
|
|
2006
|
+
|
|
2007
|
+
// Slug: artifact dir's basename, or state.json's own task_slug field — reuse whichever
|
|
2008
|
+
// this file already has, never a new slug-derivation rule.
|
|
2009
|
+
const slug = (state.task_slug && String(state.task_slug).trim()) || path.basename(artifactDir);
|
|
2010
|
+
|
|
2011
|
+
const { actor } = resolveActor(process.env);
|
|
2012
|
+
if (isUnresolvedActor(actor)) {
|
|
2013
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for "${safeOneLine(slug, 80)}" — actor identity is unresolved; never releasing under a synthetic identity.\n`);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const flowAgentsDir = path.dirname(artifactDir);
|
|
2018
|
+
const nowIso = new Date().toISOString();
|
|
2019
|
+
|
|
2020
|
+
// AC1: ALWAYS emit the liveness release, regardless of provider kind — liveness is
|
|
2021
|
+
// provider-agnostic and this is the ONE shared writer every other liveness emitter uses.
|
|
2022
|
+
try {
|
|
2023
|
+
require('./lib/liveness-write.js').appendLivenessEvent(flowAgentsDir, {
|
|
2024
|
+
type: 'release',
|
|
2025
|
+
subjectId: slug,
|
|
2026
|
+
actor,
|
|
2027
|
+
at: nowIso,
|
|
2028
|
+
source: 'stop-hook',
|
|
2029
|
+
});
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook liveness release failed for "${safeOneLine(slug, 80)}": ${safeOneLine(err && err.message || err, 160)}\n`);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Resolve the effective assignment-provider kind — unknown means "do not guess",
|
|
2035
|
+
// skip the assignment-layer release entirely (fail-open).
|
|
2036
|
+
const providerKind = resolveAssignmentProviderKind(root);
|
|
2037
|
+
if (!providerKind) {
|
|
2038
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release — provider kind unknown for "${safeOneLine(slug, 80)}", skipping assignment-layer release (liveness release already emitted).\n`);
|
|
2039
|
+
refreshHandoffAfterRelease(artifactDir, slug, state, false, null);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
let providerReleasePending = false;
|
|
2044
|
+
let providerReleaseNextCommand = null;
|
|
2045
|
+
|
|
2046
|
+
if (providerKind === 'local-file') {
|
|
2047
|
+
const provider = loadAssignmentProviderBuilt();
|
|
2048
|
+
if (!provider) {
|
|
2049
|
+
process.stderr.write('[Hook] Goal Fit: stop-hook local-file release skipped — build/src/cli/assignment-provider.js not available.\n');
|
|
2050
|
+
} else {
|
|
2051
|
+
try {
|
|
2052
|
+
// Mirror assignment-provider.ts's loadActorStruct() exactly: runtime is derived via
|
|
2053
|
+
// detectRuntime (not a literal 'unknown'), since serializeActor()'s comparison key
|
|
2054
|
+
// includes runtime — a mismatched runtime segment here would make performLocalRelease's
|
|
2055
|
+
// AC6 actor-match check wrongly treat this session's own claim as foreign.
|
|
2056
|
+
//
|
|
2057
|
+
// actorKey: opts.actorKey MUST be the same canonical `resolveActor(env).actor` string
|
|
2058
|
+
// already resolved above (`actor`) — the identical bare-or-triple form
|
|
2059
|
+
// computeEffectiveState()'s holderActorKey preference (`record.actor_key ||
|
|
2060
|
+
// serializeActor(record.actor)`) and performLocalRelease's mirrored ownership check
|
|
2061
|
+
// both key on. Without this, an explicit-override actor's release-side comparison
|
|
2062
|
+
// would fall back to serializeActor(releasedBy) — a re-derived triple that never
|
|
2063
|
+
// equals the stored bare actor_key — and this hook would wrongly treat its own claim
|
|
2064
|
+
// as foreign (the #291 seam, relocated to the release path; see performLocalRelease's
|
|
2065
|
+
// doc comment in src/cli/assignment-provider.ts).
|
|
2066
|
+
const releasedBy = { runtime: detectRuntime(process.env), session_id: actor, host: os.hostname(), human: null };
|
|
2067
|
+
const artifactRoot = flowAgentsArtifactRoot(root);
|
|
2068
|
+
provider.performLocalRelease(artifactRoot, slug, releasedBy, {
|
|
2069
|
+
reason: 'stop-hook non-terminal release',
|
|
2070
|
+
tolerateNoActiveClaim: true,
|
|
2071
|
+
actorKey: actor,
|
|
2072
|
+
});
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook local-file release failed for "${safeOneLine(slug, 80)}": ${safeOneLine(err && err.message || err, 160)}\n`);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} else {
|
|
2078
|
+
// AC3: github (or any non-local-file kind) — render-don't-execute. Never call
|
|
2079
|
+
// render-release, never invoke gh. Disclose the pending intent honestly.
|
|
2080
|
+
providerReleasePending = true;
|
|
2081
|
+
providerReleaseNextCommand = `npm run workflow -- assignment-provider status --provider github --subject-id "${slug}" ... # then: assignment-provider render-release --provider github --subject-id "${slug}" ... (emits the gh argv to run)`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// AC4: refresh handoff.json — merge, never fully overwrite.
|
|
2085
|
+
refreshHandoffAfterRelease(artifactDir, slug, state, providerReleasePending, providerReleaseNextCommand);
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
// Fail-open (AC7): any unexpected failure anywhere above (JSON parse error, fs error,
|
|
2088
|
+
// etc.) must never crash the Stop hook or affect its exit code / goal-fit's output.
|
|
2089
|
+
try {
|
|
2090
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release logic error (fail-open): ${safeOneLine(err && err.message || err, 160)}\n`);
|
|
2091
|
+
} catch { /* best-effort diagnostic only */ }
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1845
2095
|
async function run(rawInput) {
|
|
1846
2096
|
const input = parseJson(rawInput);
|
|
1847
2097
|
const root = findRepoRoot(input.cwd || process.cwd());
|
|
1848
2098
|
const mode = resolveGoalFitMode();
|
|
1849
2099
|
if (mode === 'off') return rawInput;
|
|
1850
2100
|
const result = await analyze(root);
|
|
2101
|
+
// #292 Wave 2: additive side effect only — never changes analyze()'s warnings/blocking
|
|
2102
|
+
// contract or this function's return value. Reuses analyze()'s already-resolved
|
|
2103
|
+
// latestArtifactDir rather than re-deriving a second "what is the active session" path.
|
|
2104
|
+
// Runs regardless of whether goal-fit itself has any warnings this turn (AC1: the
|
|
2105
|
+
// liveness release must always be attempted on a non-terminal Stop, independent of
|
|
2106
|
+
// whether goal-fit found anything to warn about).
|
|
2107
|
+
releaseOnNonTerminalStop(root, result.latestArtifactDir || null);
|
|
1851
2108
|
if (result.warnings.length === 0) {
|
|
1852
2109
|
clearBlockStreak(root);
|
|
1853
2110
|
return rawInput;
|
|
@@ -1937,4 +2194,4 @@ if (require.main === module) {
|
|
|
1937
2194
|
});
|
|
1938
2195
|
}
|
|
1939
2196
|
|
|
1940
|
-
module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator };
|
|
2197
|
+
module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator, releaseOnNonTerminalStop };
|
|
@@ -19,6 +19,7 @@ const path = require('path');
|
|
|
19
19
|
const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
|
|
20
20
|
const { resolveActor } = require('./lib/actor-identity');
|
|
21
21
|
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
22
|
+
const { readCurrentPointer } = require('./lib/current-pointer');
|
|
22
23
|
|
|
23
24
|
const STEERING = {
|
|
24
25
|
'tool-planner': [
|
|
@@ -102,7 +103,48 @@ function readJson(file) {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* #291 (Conflict #2, Wave 2 Task 2.4): actor-scoped preference consulted BEFORE the global
|
|
108
|
+
* newest-mtime scan below. For each read-root, resolve the "current" pointer via
|
|
109
|
+
* `readCurrentPointer` (per-actor `current/<actor>.json` preferred, legacy global
|
|
110
|
+
* `current.json` fallback — the same single choke point every other Wave 2 consumer uses) and,
|
|
111
|
+
* if it names a session dir whose `state.json` is still in an ACTIVE_STATE_STATUSES status,
|
|
112
|
+
* return that state immediately — so actor A's own ambient steering hint always prefers A's own
|
|
113
|
+
* current task over a globally-newer-mtime `state.json` some other actor (B) happened to touch
|
|
114
|
+
* more recently. Returns null (never throws) when no root yields an actor-scoped resolution,
|
|
115
|
+
* so the caller falls through to the unchanged global scan below — this is the compat-shim
|
|
116
|
+
* guarantee: when `actorKey` is empty/unresolved and/or no per-actor file exists yet, this
|
|
117
|
+
* degrades to the SAME legacy-`current.json`-or-nothing lookup the compat shim already performs
|
|
118
|
+
* for every other Wave 2 reader, so a single-session/CI/legacy-only fixture cannot fail to
|
|
119
|
+
* resolve here in a way it wouldn't already have resolved via the global scan.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} root
|
|
122
|
+
* @param {string} actorKey
|
|
123
|
+
* @returns {{file: string, payload: object, mtimeMs: number}|null}
|
|
124
|
+
*/
|
|
125
|
+
function actorScopedWorkflowState(root, actorKey) {
|
|
126
|
+
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
127
|
+
const { payload: current } = readCurrentPointer(flowAgentsDir, actorKey);
|
|
128
|
+
if (!current) continue;
|
|
129
|
+
const slug = current.artifact_dir || current.active_slug;
|
|
130
|
+
if (typeof slug !== 'string' || !slug.trim()) continue;
|
|
131
|
+
const safe = slug.replace(/\.\.+/g, '').replace(/^[/\\]+/, '');
|
|
132
|
+
const dir = path.join(flowAgentsDir, safe);
|
|
133
|
+
if (!dir.startsWith(flowAgentsDir + path.sep) || !fs.existsSync(dir)) continue;
|
|
134
|
+
const file = path.join(dir, 'state.json');
|
|
135
|
+
let stat;
|
|
136
|
+
try { stat = fs.statSync(file); } catch { continue; }
|
|
137
|
+
const payload = readJson(file);
|
|
138
|
+
if (!payload || !ACTIVE_STATE_STATUSES.has(payload.status)) continue;
|
|
139
|
+
return { file, payload, mtimeMs: stat.mtimeMs };
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
function latestWorkflowState(root) {
|
|
145
|
+
const preferred = actorScopedWorkflowState(root, resolveActor(process.env).actor);
|
|
146
|
+
if (preferred) return preferred;
|
|
147
|
+
|
|
106
148
|
const states = flowAgentsArtifactRootsForRead(root)
|
|
107
149
|
.flatMap(artifactRoot => walkStateFiles(artifactRoot))
|
|
108
150
|
.map(file => {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: ADR 0001: Flow Agents Consumes Flow For Workflow Enforcement
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0001: Flow Agents Consumes Flow For Workflow Enforcement
|
|
6
8
|
|
|
7
9
|
Date: 2026-05-24
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: ADR 0002: Flow Kits As The Extension Unit
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0002: Flow Kits As The Extension Unit
|
|
6
8
|
|
|
7
9
|
Flow Agents will use **Flow Kit** as the product and implementation term for installable workflow bundles, replacing the older "pack" vocabulary. A Flow Kit may contain Flow Definitions, skills, adapters, provider contracts, docs, and evals; the Kit Catalog lists available kits and installable assets, but the workflow semantics live in kit-owned Flow Definition files. This avoids overloading packaging metadata with process semantics and gives custom workflow authoring a clearer product language.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: ADR 0003: Flow Agents Coordinates Kits And Adapters
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0003: Flow Agents Coordinates Kits And Adapters
|
|
6
8
|
|
|
7
9
|
Flow Agents will not own the builder, or knowledge workflows as core behavior. Those out-of-the-box behaviors will be extracted into normal Flow Kits that use the same manifest, Flow Definition, skill, doc, provider, and eval contracts as third-party kits. Flow Agents owns kit validation, installation, runtime adapter selection, provider wiring, status/control commands, and runtime-specific export; Runtime Adapters own target-specific integration for local agent runtimes and API/framework agents.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: ADR 0004: Gates Expect Hachure Trust Bundles
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0004: Gates Expect Hachure Trust Bundles
|
|
6
8
|
|
|
7
9
|
Flow-backed kits will model rich gate evidence as claim expectations using the Hachure trust.bundle format rather than provider-specific requirements. A gate expectation can require `kind: "trust.bundle"`, a domain claim type such as `builder.verify.tests`, accepted trust statuses such as `verified`, and whether the expectation blocks the transition; project or runtime config maps claim types to trusted Surface producers and authority traces. This lets the Builder Kit use repo governance, command checks, CI, human decisions, or future producers without naming a specific provider in the Flow Definition.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: ADR 0005: Kubernetes-Inspired Kontour Resource Contracts
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0005: Kubernetes-Inspired Kontour Resource Contracts
|
|
6
8
|
|
|
7
9
|
Date: 2026-05-27
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0007: Flow / Skill / Kit / Tool Boundary"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0007: Flow / Skill / Kit / Tool Boundary
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-15
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "Skill Audit 2026-06-15: Flow / Skill / Kit / Tool Boundary"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# Skill Audit: Flow / Skill / Kit / Tool Boundary
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-15
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0010: Workflow Trust State as a Hachure Trust Bundle"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0010: Workflow Trust State as a Hachure Trust Bundle
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-23
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0011: MCP Posture — Enforcement Stays Hooks; Surface Owns MCP Projection; No Auto-Injected Config"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0011: MCP Posture
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-24
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0012: Agent Coordination as Hachure Liveness Claims"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0012: Agent Coordination as Hachure Liveness Claims
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-24
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0013: Context Lifecycle — Workflow-Boundary Compaction, Freshness-Gated Reuse, and the Learning Split"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0013: Context Lifecycle
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-25
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0014: Flow Agents core vs domain kits — the generic/kit boundary"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0014: Flow Agents core vs domain kits
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-25
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0015: Flow / Flow Agents Boundary Reconciliation"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0015: Flow / Flow Agents Boundary Reconciliation
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-25
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0016: The Three-Hard-Boundary Model — a FlowDefinition-Driven, Kit-Agnostic Core"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0016: The Three-Hard-Boundary Model — a FlowDefinition-Driven, Kit-Agnostic Core
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-06-26
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0017: The Anti-Gaming Trust Security Model — Layered Defense + External CI Anchor"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0017: The Anti-Gaming Trust Security Model — Layered Defense + External CI Anchor
|
|
6
8
|
|
|
7
9
|
Status: Accepted
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0018: Freeze the Local Shell-Parsing Heuristics; Route New Enforcement to the CI Anchor
|
|
6
8
|
|
|
7
9
|
Status: Accepted
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0020: Trust-Reconcile Manifest, Claim Classification, and Waivers
|
|
6
8
|
|
|
7
9
|
Status: Accepted
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: "ADR 0021: Assignment Leases and Stale-Claim Takeover"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
> **FROZEN — immutable history.** Superseding/current decisions live in [`docs/decisions/`](../decisions/index.md). Do not edit.
|
|
6
|
+
|
|
5
7
|
# ADR 0021: Assignment Leases and Stale-Claim Takeover
|
|
6
8
|
|
|
7
9
|
**Date:** 2026-07-02
|
|
@@ -98,7 +100,7 @@ Five touchpoints, one policy:
|
|
|
98
100
|
|---|---|---|
|
|
99
101
|
| `workflow-steering.js`, every turn | read | one-line liveness digest; "superseded" interrupt for the held subject |
|
|
100
102
|
| `pull-work` | read + write | exclude held; claim (liveness + provider) on selection |
|
|
101
|
-
| `ensure-session` / pickup | read + write | refuse entry to a session dir under a fresh other-actor claim; supersede-then-enter if stale |
|
|
103
|
+
| `ensure-session` / pickup | read + write | refuse entry to a session dir under a fresh other-actor claim; supersede-then-enter if stale (**implemented by #291**) |
|
|
102
104
|
| `advance-state` + tool activity | write | heartbeat rides existing writes; no bespoke timer |
|
|
103
105
|
| **publish (push / PR / merge)** | **read, blocking** | **verify-hold**: hard-stop if not the fresh, non-superseded holder |
|
|
104
106
|
|