@kontourai/flow-agents 3.4.3 → 3.6.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 +4 -0
- package/CHANGELOG.md +15 -0
- package/build/src/builder-flow-run-adapter.d.ts +10 -1
- package/build/src/builder-flow-run-adapter.js +29 -1
- package/build/src/builder-flow-runtime.d.ts +18 -0
- package/build/src/builder-flow-runtime.js +205 -13
- package/build/src/builder-lifecycle-authority.d.ts +35 -0
- package/build/src/builder-lifecycle-authority.js +219 -0
- package/build/src/cli/assignment-provider.d.ts +13 -0
- package/build/src/cli/assignment-provider.js +120 -62
- package/build/src/cli/builder-run.js +46 -5
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +3 -0
- package/build/src/cli/workflow-sidecar.js +140 -30
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +521 -0
- package/build/src/cli.js +2 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +2 -0
- package/build/src/lib/flow-resolver.js +7 -2
- package/build/src/lib/package-version.d.ts +2 -0
- package/build/src/lib/package-version.js +13 -0
- package/build/src/lib/pinned-cli-command.d.ts +6 -0
- package/build/src/lib/pinned-cli-command.js +21 -0
- package/context/contracts/artifact-contract.md +1 -1
- package/context/contracts/assignment-provider-contract.md +5 -2
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/stop-goal-fit.js +1 -1
- package/docs/context-map.md +2 -0
- package/docs/public-workflow-cli.md +63 -0
- package/docs/spec/builder-flow-runtime.md +49 -5
- package/docs/workflow-usage-guide.md +5 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -24
- package/evals/integration/test_bundle_install.sh +97 -0
- package/evals/integration/test_current_json_per_actor.sh +1 -0
- package/evals/integration/test_dual_emit_flow_step.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
- package/evals/integration/test_phase_map_and_gate_claim.sh +4 -0
- package/evals/integration/test_public_workflow_cli.sh +259 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1 -0
- package/package.json +2 -2
- package/schemas/builder-lifecycle-authorization.schema.json +57 -0
- package/schemas/lifecycle-authority-keys.schema.json +25 -0
- package/schemas/workflow-state.schema.json +1 -1
- package/scripts/hooks/config-protection.js +8 -1
- package/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/scripts/hooks/stop-goal-fit.js +1 -1
- package/src/builder-flow-run-adapter.ts +47 -0
- package/src/builder-flow-runtime.ts +216 -4
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider.ts +84 -20
- package/src/cli/builder-flow-runtime.test.mjs +404 -1
- package/src/cli/builder-run.ts +56 -5
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar.ts +138 -31
- package/src/cli/workflow.ts +471 -0
- package/src/cli.ts +2 -0
- package/src/index.ts +14 -0
- package/src/lib/flow-resolver.ts +6 -2
- package/src/lib/package-version.ts +15 -0
- package/src/lib/pinned-cli-command.ts +23 -0
|
@@ -9,17 +9,21 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
10
10
|
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
|
|
11
11
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
12
|
+
import { ensureSafeDirectory } from "../lib/fs.js";
|
|
13
|
+
import { flowAgentsPackageVersion } from "../lib/package-version.js";
|
|
14
|
+
import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
|
|
12
15
|
import { startBuilderFlowSession, syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
|
|
13
16
|
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
14
17
|
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
15
18
|
// `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
|
|
16
19
|
// import — same idiom already used above for ../lib/flow-resolver.js).
|
|
17
|
-
import { computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus } from "./assignment-provider.js";
|
|
18
|
-
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
20
|
+
import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus, withSubjectLock } from "./assignment-provider.js";
|
|
21
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "canceled", "accepted", "archived"]);
|
|
19
22
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
20
23
|
export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
21
24
|
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
22
25
|
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
26
|
+
export const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
|
|
23
27
|
function now() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
24
28
|
function read(file) { return fs.readFileSync(file, "utf8"); }
|
|
25
29
|
export function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
@@ -55,14 +59,27 @@ function workItemSlug(ref) {
|
|
|
55
59
|
const [owner, repo] = parts;
|
|
56
60
|
return slugify(`${owner}-${repo}-${id}`, "work-item");
|
|
57
61
|
}
|
|
62
|
+
function assignmentSubjectMatchesWorkItem(slug, ref) {
|
|
63
|
+
if (ref === `local:${slug}`)
|
|
64
|
+
return true;
|
|
65
|
+
if (!/^[^/#]+\/[^/#]+#\d+$/.test(ref))
|
|
66
|
+
return false;
|
|
67
|
+
return workItemSlug(ref) === slug;
|
|
68
|
+
}
|
|
58
69
|
function sessionWorkItem(p, slug, dir) {
|
|
59
70
|
const providerRef = opt(p, "work-item");
|
|
71
|
+
const existingState = loadJson(path.join(dir, "state.json"));
|
|
72
|
+
const existingRef = Array.isArray(existingState.work_item_refs) && typeof existingState.work_item_refs[0] === "string"
|
|
73
|
+
? existingState.work_item_refs[0]
|
|
74
|
+
: "";
|
|
60
75
|
if (providerRef) {
|
|
76
|
+
if (existingRef && existingRef !== providerRef) {
|
|
77
|
+
die(`ensure-session refused: session ${JSON.stringify(slug)} is already bound to Work Item ${JSON.stringify(existingRef)}, not ${JSON.stringify(providerRef)}`);
|
|
78
|
+
}
|
|
61
79
|
return { ref: providerRef };
|
|
62
80
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return { ref: existingState.work_item_refs[0] };
|
|
81
|
+
if (existingRef) {
|
|
82
|
+
return { ref: existingRef };
|
|
66
83
|
}
|
|
67
84
|
const title = opt(p, "title", slug).trim() || slug;
|
|
68
85
|
const body = opt(p, "summary").trim();
|
|
@@ -1437,6 +1454,15 @@ function sessionDirFor(root, slug) {
|
|
|
1437
1454
|
die("session directory must stay under artifact root");
|
|
1438
1455
|
return dir;
|
|
1439
1456
|
}
|
|
1457
|
+
function assertSafeSessionDirectory(root, dir) {
|
|
1458
|
+
if (!fs.existsSync(dir))
|
|
1459
|
+
return;
|
|
1460
|
+
const stat = fs.lstatSync(dir);
|
|
1461
|
+
const expected = path.join(fs.realpathSync(root), path.basename(dir));
|
|
1462
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(dir) !== expected) {
|
|
1463
|
+
die(`session directory must be a real directory under the artifact root: ${dir}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1440
1466
|
function validateAgentId(agent) {
|
|
1441
1467
|
if (!agent)
|
|
1442
1468
|
die("--agent-id is required");
|
|
@@ -1481,17 +1507,22 @@ function findRepoRootFromDirStrict(startDir) {
|
|
|
1481
1507
|
* Find the repository root by walking upward from a starting directory to locate
|
|
1482
1508
|
* the nearest ancestor containing a kits/ subdirectory. Mirrors flow-resolver.ts
|
|
1483
1509
|
* findRepoRoot, but callable from workflow-sidecar.ts without re-importing the
|
|
1484
|
-
* internal helper.
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1510
|
+
* internal helper. A canonical `.kontourai` path falls back to its owning project,
|
|
1511
|
+
* allowing installed package assets to resolve independently of ambient cwd. Other
|
|
1512
|
+
* layouts retain the cwd fallback used by standalone primitive sessions.
|
|
1487
1513
|
*
|
|
1488
|
-
* Do NOT use this
|
|
1489
|
-
*
|
|
1514
|
+
* Do NOT use this permissive variant for publishDelivery's repo-root resolution —
|
|
1515
|
+
* use findRepoRootFromDirStrict there instead, so a scratch/test
|
|
1490
1516
|
* session dir with no repo ancestor fails closed (skips publish) rather than
|
|
1491
1517
|
* silently trusting process.cwd(), which could be an unrelated real repo.
|
|
1492
1518
|
*/
|
|
1493
1519
|
function findRepoRootFromDir(startDir) {
|
|
1494
|
-
|
|
1520
|
+
const discovered = findRepoRootFromDirStrict(startDir);
|
|
1521
|
+
if (discovered)
|
|
1522
|
+
return discovered;
|
|
1523
|
+
if (path.basename(startDir) === ".kontourai")
|
|
1524
|
+
return path.dirname(startDir);
|
|
1525
|
+
return process.cwd();
|
|
1495
1526
|
}
|
|
1496
1527
|
/**
|
|
1497
1528
|
* Resolve the first step id from a FlowDefinition's steps[] list.
|
|
@@ -1653,6 +1684,10 @@ function currentDir(root, actorKey) {
|
|
|
1653
1684
|
}
|
|
1654
1685
|
return dir;
|
|
1655
1686
|
}
|
|
1687
|
+
export function currentWorkflowSessionDir(root) {
|
|
1688
|
+
const resolved = loadActorIdentityHelper().resolveActor(process.env);
|
|
1689
|
+
return currentDir(path.resolve(root), loadActorIdentityHelper().isUnresolvedActor(resolved.actor) ? undefined : resolved.actor);
|
|
1690
|
+
}
|
|
1656
1691
|
/**
|
|
1657
1692
|
* #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
|
|
1658
1693
|
* exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
|
|
@@ -1770,10 +1805,10 @@ function loadJsonInputFile(file) {
|
|
|
1770
1805
|
* mitigation class: a hostile liveness event or a hand-crafted --effective-state-json fixture must
|
|
1771
1806
|
* never be able to smuggle raw control/ANSI bytes into this process's stderr/thrown message).
|
|
1772
1807
|
*/
|
|
1773
|
-
function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
1808
|
+
function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemRef) {
|
|
1774
1809
|
if (p.flags.has("skip-ownership-guard")) {
|
|
1775
1810
|
process.stderr.write("[ensure-session] ownership guard skipped via --skip-ownership-guard\n");
|
|
1776
|
-
return;
|
|
1811
|
+
return null;
|
|
1777
1812
|
}
|
|
1778
1813
|
// Design Decision 4 (unchanged from resolveSessionBranch): actor-resolution ambiguity never
|
|
1779
1814
|
// hard-fails session creation. Without a resolvable actor there is no safe identity to claim
|
|
@@ -1781,7 +1816,7 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1781
1816
|
// than claiming under a synthetic/unstable identity.
|
|
1782
1817
|
if (resolution.unresolved) {
|
|
1783
1818
|
process.stderr.write("[ensure-session] ownership guard not evaluated: actor is unresolved (set --actor or FLOW_AGENTS_ACTOR, or run inside a supported runtime) — proceeding without a durable claim, exactly as ensure-session behaved before #291\n");
|
|
1784
|
-
return;
|
|
1819
|
+
return null;
|
|
1785
1820
|
}
|
|
1786
1821
|
// F5 fix (fix-plan iteration 1, LOW): match assignment-provider.ts's sanitizeDisplayField
|
|
1787
1822
|
// two-tier convention (64 for id-like fields, 240 for free text) rather than asserting one
|
|
@@ -1830,6 +1865,9 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1830
1865
|
// `assignment-provider status --self-actor <branchActorKey>` run by a DIFFERENT tool
|
|
1831
1866
|
// afterward would never recognize this guard's own claim as self.
|
|
1832
1867
|
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1868
|
+
if (workItemRef && assignment.record?.work_item_ref && assignment.record.work_item_ref !== workItemRef) {
|
|
1869
|
+
die(`ensure-session refused: assignment ${JSON.stringify(slug)} is already bound to Work Item ${JSON.stringify(assignment.record.work_item_ref)}, not ${JSON.stringify(workItemRef)}`);
|
|
1870
|
+
}
|
|
1833
1871
|
const events = readLivenessEvents(root);
|
|
1834
1872
|
const freshList = loadLivenessReadHelper().freshHolders(events, slug, resolution.branchActorKey, nowMs);
|
|
1835
1873
|
effective = computeEffectiveState(assignment, freshList, resolution.branchActorKey, nowMs);
|
|
@@ -1841,8 +1879,27 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1841
1879
|
// neither applies, this is a documented scope boundary (today's pre-#291 baseline behavior),
|
|
1842
1880
|
// never a silent hole.
|
|
1843
1881
|
process.stderr.write(`[ensure-session] ownership guard not evaluated: provider "${sanitize(assignmentProviderKind)}" requires --effective-state-json\n`);
|
|
1844
|
-
return;
|
|
1882
|
+
return null;
|
|
1845
1883
|
}
|
|
1884
|
+
const selectedWorkEvidence = () => {
|
|
1885
|
+
// Only a claim read from the local provider is durable evidence. Precomputed provider state
|
|
1886
|
+
// can authorize entry, but it cannot prove that this process acquired the Work Item.
|
|
1887
|
+
if (!workItemRef || effectiveStateJsonFlag || assignmentProviderKind !== "local-file")
|
|
1888
|
+
return null;
|
|
1889
|
+
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1890
|
+
const record = assignment.record;
|
|
1891
|
+
if (!record
|
|
1892
|
+
|| record.status !== "claimed"
|
|
1893
|
+
|| record.subject_id !== slug
|
|
1894
|
+
|| record.actor_key !== resolution.branchActorKey
|
|
1895
|
+
|| record.work_item_ref !== workItemRef) {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
assignmentFile: assignmentFilePath(root, slug),
|
|
1900
|
+
actorKey: resolution.branchActorKey,
|
|
1901
|
+
};
|
|
1902
|
+
};
|
|
1846
1903
|
const resolveBranchForClaim = () => {
|
|
1847
1904
|
const existingBranch = fs.existsSync(path.join(dir, "state.json")) ? loadJson(path.join(dir, "state.json")).branch : undefined;
|
|
1848
1905
|
return existingBranch || resolveSessionBranch(p, slug);
|
|
@@ -1855,17 +1912,17 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1855
1912
|
// effective_state computation instead of silently using a different identity.
|
|
1856
1913
|
const isSelf = effective.reason === "self_is_holder" || (!!effective.holder?.actor && effective.holder.actor === resolution.branchActorKey);
|
|
1857
1914
|
if (isSelf)
|
|
1858
|
-
return;
|
|
1915
|
+
return selectedWorkEvidence();
|
|
1859
1916
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1860
1917
|
const lastAtSuffix = effective.holder?.last_at ? ` (last_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1861
1918
|
die(`ensure-session refused: subject ${sanitize(slug)} is currently held by a different, still-live actor (${holderActor}${lastAtSuffix}). Pick a different work item, or confirm the holder session is truly gone before considering a takeover.`);
|
|
1862
|
-
return;
|
|
1919
|
+
return null;
|
|
1863
1920
|
}
|
|
1864
1921
|
case "human-held": {
|
|
1865
1922
|
const assignee = effective.holder?.assignee ? sanitize(effective.holder.assignee) : "an assigned human";
|
|
1866
1923
|
const idleSuffix = effective.holder?.idle_days != null ? ` (idle ${Number(effective.holder.idle_days)} day(s))` : "";
|
|
1867
1924
|
die(`ensure-session refused: subject ${sanitize(slug)} is assigned to ${assignee}${idleSuffix}. This guard never auto-reclaims a human assignment — confirm with the user before proceeding.`);
|
|
1868
|
-
return;
|
|
1925
|
+
return null;
|
|
1869
1926
|
}
|
|
1870
1927
|
case "reclaimable": {
|
|
1871
1928
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
@@ -1899,11 +1956,12 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1899
1956
|
// computeEffectiveState's holderActorKey (assignment-provider.ts) matches this same
|
|
1900
1957
|
// branchActorKey string on the next status/guard check, cross-tool.
|
|
1901
1958
|
actorKey: resolution.branchActorKey,
|
|
1959
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1902
1960
|
});
|
|
1903
1961
|
// Render-don't-execute: emit the incumbent's branch so the skill continues it (never a new
|
|
1904
1962
|
// branch). The successor re-enters the SAME artifact dir (deterministic slug) by construction.
|
|
1905
1963
|
printJson({ role: "SupersedeTakeover", subject: sanitize(slug), superseded_actor: holderActor, ...(incumbentBranch ? { resumed_branch: sanitizeWide(incumbentBranch) } : {}), reason: sanitizeWide(takeoverReason) });
|
|
1906
|
-
return;
|
|
1964
|
+
return selectedWorkEvidence();
|
|
1907
1965
|
}
|
|
1908
1966
|
case "free": {
|
|
1909
1967
|
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
@@ -1913,8 +1971,9 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
|
1913
1971
|
reason: opt(p, "reason", "ensure-session entry"),
|
|
1914
1972
|
// F1 fix (fix-plan iteration 1, HIGH): see the performLocalSupersede call above.
|
|
1915
1973
|
actorKey: resolution.branchActorKey,
|
|
1974
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1916
1975
|
});
|
|
1917
|
-
return;
|
|
1976
|
+
return selectedWorkEvidence();
|
|
1918
1977
|
}
|
|
1919
1978
|
default:
|
|
1920
1979
|
die(`ensure-session ownership guard: unrecognized effective_state ${JSON.stringify(effective.effective_state)}`);
|
|
@@ -1993,6 +2052,7 @@ function preflightEnsureSession(p) {
|
|
|
1993
2052
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
1994
2053
|
const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
|
|
1995
2054
|
const dir = sessionDirFor(root, slug);
|
|
2055
|
+
assertSafeSessionDirectory(root, dir);
|
|
1996
2056
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
1997
2057
|
if (entry.flowId === "builder.build")
|
|
1998
2058
|
assertCanonicalBuilderArtifactRoot(root);
|
|
@@ -2002,6 +2062,7 @@ async function ensureSession(p) {
|
|
|
2002
2062
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2003
2063
|
const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
|
|
2004
2064
|
const dir = sessionDirFor(root, slug);
|
|
2065
|
+
assertSafeSessionDirectory(root, dir);
|
|
2005
2066
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2006
2067
|
if (entry.flowId === "builder.build")
|
|
2007
2068
|
assertCanonicalBuilderArtifactRoot(root);
|
|
@@ -2011,8 +2072,11 @@ async function ensureSession(p) {
|
|
|
2011
2072
|
// below (writeCurrent's per-actor dual-write) so the branch-naming actor and the
|
|
2012
2073
|
// assignment-claim actor are always the same identity.
|
|
2013
2074
|
const actorResolution = resolveEnsureSessionActor(p);
|
|
2014
|
-
|
|
2015
|
-
|
|
2075
|
+
const selectionWorkItemRef = entry.flowId === "builder.build" && assignmentSubjectMatchesWorkItem(slug, workItem.ref)
|
|
2076
|
+
? workItem.ref
|
|
2077
|
+
: undefined;
|
|
2078
|
+
const selectedWorkEvidence = enforceEnsureSessionOwnership(p, root, slug, dir, actorResolution, selectionWorkItemRef);
|
|
2079
|
+
ensureSafeDirectory(root, dir);
|
|
2016
2080
|
const timestamp = opt(p, "timestamp", now());
|
|
2017
2081
|
if (workItem.localRecord && !fs.existsSync(path.join(dir, "work-item.json"))) {
|
|
2018
2082
|
writeJson(path.join(dir, "work-item.json"), workItem.localRecord);
|
|
@@ -2025,13 +2089,25 @@ async function ensureSession(p) {
|
|
|
2025
2089
|
// takeover continuity true by construction (see Design Decision 3 in the plan).
|
|
2026
2090
|
const branch = resolveSessionBranch(p, slug);
|
|
2027
2091
|
const initialMarkdownStatus = entry.flowId ? "new" : "planning";
|
|
2028
|
-
|
|
2092
|
+
const acceptanceCriteria = opts(p, "criterion");
|
|
2093
|
+
if (acceptanceCriteria.length === 0)
|
|
2094
|
+
acceptanceCriteria.push(`Complete the requested outcome: ${opt(p, "summary", "Workflow session is durable.")}`);
|
|
2095
|
+
md = `# ${opt(p, "title", slug)}\n\nbranch: ${branch}\nworktree: main\ncreated: ${timestamp}\nstatus: ${initialMarkdownStatus}\ntype: deliver\niteration: 1\n\n## Plan\n\n${opt(p, "summary", "")}\n\n## Definition Of Done\n\n- **User outcome:** ${opt(p, "summary", "Workflow session is durable.")}\n- **Scope:** Workflow session artifacts and sidecars.\n- **Acceptance criteria:**\n${acceptanceCriteria.map((c) => ` - [ ] ${c} - Evidence: pending.`).join("\n")}\n- **Usefulness checks:**\n - [ ] User-facing workflow is documented or discoverable\n- **Stop-short risks:** Workflow artifacts could drift.\n- **Durable docs target:** not needed\n- **Sandbox mode:** local-edit\n\n## Execution Progress\n\n- [ ] Session initialized.\n\n## Verification Report\n\nBuild: [NOT_VERIFIED] Verification has not run yet.\n\n### Acceptance Criteria\n- [NOT_VERIFIED] Verification has not run yet - Evidence: pending workflow execution and checks.\n\n### Verdict: NOT_VERIFIED\n\n## Goal Fit Gate\n\n- [ ] Original user goal restated\n\n## Final Acceptance\n\n- [ ] CI/relevant checks passed or local equivalent recorded\n`;
|
|
2029
2096
|
fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), md);
|
|
2030
2097
|
}
|
|
2031
2098
|
if (!fs.existsSync(path.join(dir, "state.json")) || !fs.existsSync(path.join(dir, "acceptance.json")) || !fs.existsSync(path.join(dir, "handoff.json"))) {
|
|
2032
2099
|
const phaseMap = entry.flowId ? resolvePhaseMap(entry.flowId, findRepoRootFromDir(dir)) : null;
|
|
2033
2100
|
const initialPhase = Object.entries(phaseMap ?? {}).find(([, step]) => step === entry.firstStep)?.[0];
|
|
2034
|
-
const startCommand =
|
|
2101
|
+
const startCommand = pinnedFlowAgentsCommand(flowAgentsPackageVersion(), [
|
|
2102
|
+
"workflow", "start",
|
|
2103
|
+
"--flow", "builder.build",
|
|
2104
|
+
"--work-item", workItem.ref,
|
|
2105
|
+
...(workItem.ref === `local:${slug}` ? ["--task-slug", slug] : []),
|
|
2106
|
+
"--artifact-root", root,
|
|
2107
|
+
"--title", opt(p, "title", slug),
|
|
2108
|
+
"--summary", opt(p, "summary", "Workflow session is durable."),
|
|
2109
|
+
...opts(p, "criterion").flatMap((criterion) => ["--criterion", criterion]),
|
|
2110
|
+
]);
|
|
2035
2111
|
const nextAction = entry.flowId
|
|
2036
2112
|
? entry.flowId === "builder.build"
|
|
2037
2113
|
? {
|
|
@@ -2061,11 +2137,42 @@ async function ensureSession(p) {
|
|
|
2061
2137
|
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", entry.flowId || undefined, resumedStep || undefined, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
2062
2138
|
if (entry.flowId === "builder.build") {
|
|
2063
2139
|
try {
|
|
2064
|
-
await startBuilderFlowSession({ sessionDir: dir });
|
|
2140
|
+
const started = await startBuilderFlowSession({ sessionDir: dir });
|
|
2141
|
+
if (started.run.state.current_step === "pull-work"
|
|
2142
|
+
&& selectedWorkEvidence
|
|
2143
|
+
&& assignmentSubjectMatchesWorkItem(slug, workItem.ref)) {
|
|
2144
|
+
await withSubjectLock(root, slug, async () => {
|
|
2145
|
+
const assignment = readLocalAssignmentStatus(root, slug).record;
|
|
2146
|
+
if (!assignment
|
|
2147
|
+
|| assignment.status !== "claimed"
|
|
2148
|
+
|| assignment.subject_id !== slug
|
|
2149
|
+
|| assignment.actor_key !== selectedWorkEvidence.actorKey
|
|
2150
|
+
|| assignment.work_item_ref !== workItem.ref) {
|
|
2151
|
+
die(`ensure-session refused to emit selected-work evidence: assignment provenance changed before gate evaluation for ${JSON.stringify(workItem.ref)}`);
|
|
2152
|
+
}
|
|
2153
|
+
const assignmentContent = fs.readFileSync(selectedWorkEvidence.assignmentFile);
|
|
2154
|
+
const assignmentDigest = createHash("sha256").update(assignmentContent).digest("hex");
|
|
2155
|
+
const projectRoot = path.dirname(path.dirname(root));
|
|
2156
|
+
const evidenceFile = path.relative(projectRoot, selectedWorkEvidence.assignmentFile);
|
|
2157
|
+
await recordGateClaim(parseArgs([
|
|
2158
|
+
"record-gate-claim",
|
|
2159
|
+
dir,
|
|
2160
|
+
"--actor", selectedWorkEvidence.actorKey,
|
|
2161
|
+
"--expectation", "selected-work",
|
|
2162
|
+
"--status", "pass",
|
|
2163
|
+
"--summary", `Work Item ${workItem.ref} is durably assigned to the active workflow actor (assignment sha256:${assignmentDigest}).`,
|
|
2164
|
+
"--evidence-ref-json", JSON.stringify({
|
|
2165
|
+
kind: "artifact",
|
|
2166
|
+
file: evidenceFile,
|
|
2167
|
+
summary: `Durable local assignment record for the selected Work Item and active actor; sha256:${assignmentDigest}.`,
|
|
2168
|
+
}),
|
|
2169
|
+
"--timestamp", timestamp,
|
|
2170
|
+
]));
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2065
2173
|
}
|
|
2066
2174
|
catch (error) {
|
|
2067
|
-
|
|
2068
|
-
process.stderr.write(`[ensure-session] canonical Builder Flow run did not start; the sidecar remains retryable. Run: ${retry}\n`);
|
|
2175
|
+
process.stderr.write("[ensure-session] canonical Builder Flow entry failed; the acquired Work Item provenance remains retryable. Re-run the same ensure-session command.\n");
|
|
2069
2176
|
throw error;
|
|
2070
2177
|
}
|
|
2071
2178
|
}
|
|
@@ -5308,7 +5415,7 @@ function loadLivenessReadHelper() {
|
|
|
5308
5415
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
5309
5416
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
5310
5417
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
5311
|
-
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
5418
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "archived"]);
|
|
5312
5419
|
/**
|
|
5313
5420
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
5314
5421
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|
|
@@ -5752,8 +5859,8 @@ Available claim ids:
|
|
|
5752
5859
|
return 0;
|
|
5753
5860
|
}
|
|
5754
5861
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5755
|
-
async function main() {
|
|
5756
|
-
const _rawArgv =
|
|
5862
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
5863
|
+
const _rawArgv = argv;
|
|
5757
5864
|
// #380: `record-check <dir> -- <command...>` — argv after the FIRST literal `--` token is the
|
|
5758
5865
|
// command to execute verbatim (never option-parsed: a command like `npm test -- --watch`
|
|
5759
5866
|
// legitimately contains its OWN `--`, so only the record-check dispatcher's own separator, the
|
|
@@ -5839,5 +5946,8 @@ catch {
|
|
|
5839
5946
|
return process.argv[1];
|
|
5840
5947
|
} })();
|
|
5841
5948
|
if (_selfRealPath === _argv1RealPath) {
|
|
5949
|
+
if (path.basename(process.argv[1] ?? "") === "flow-agents-workflow-sidecar") {
|
|
5950
|
+
process.stderr.write("flow-agents-workflow-sidecar is deprecated; use `flow-agents workflow` or an explicitly pinned `npx @kontourai/flow-agents@<version> workflow` command.\n");
|
|
5951
|
+
}
|
|
5842
5952
|
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
5843
5953
|
}
|