@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,20 +9,24 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
10
10
|
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy, type ActiveFlowStep } 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, type ActorStruct, type EffectiveState, type FreshHolder } from "./assignment-provider.js";
|
|
20
|
+
import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus, withSubjectLock, type ActorStruct, type EffectiveState, type FreshHolder } from "./assignment-provider.js";
|
|
18
21
|
|
|
19
22
|
type AnyObj = Record<string, any>;
|
|
20
23
|
|
|
21
|
-
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
24
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "canceled", "accepted", "archived"]);
|
|
22
25
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
23
26
|
export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
24
27
|
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
25
28
|
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
29
|
+
export const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
|
|
26
30
|
|
|
27
31
|
function now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
28
32
|
function read(file: string): string { return fs.readFileSync(file, "utf8"); }
|
|
@@ -57,6 +61,12 @@ function workItemSlug(ref: string): string {
|
|
|
57
61
|
return slugify(`${owner}-${repo}-${id}`, "work-item");
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function assignmentSubjectMatchesWorkItem(slug: string, ref: string): boolean {
|
|
65
|
+
if (ref === `local:${slug}`) return true;
|
|
66
|
+
if (!/^[^/#]+\/[^/#]+#\d+$/.test(ref)) return false;
|
|
67
|
+
return workItemSlug(ref) === slug;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
type SessionWorkItem = {
|
|
61
71
|
ref: string;
|
|
62
72
|
localRecord?: AnyObj;
|
|
@@ -64,13 +74,18 @@ type SessionWorkItem = {
|
|
|
64
74
|
|
|
65
75
|
function sessionWorkItem(p: ReturnType<typeof parseArgs>, slug: string, dir: string): SessionWorkItem {
|
|
66
76
|
const providerRef = opt(p, "work-item");
|
|
77
|
+
const existingState = loadJson(path.join(dir, "state.json"));
|
|
78
|
+
const existingRef = Array.isArray(existingState.work_item_refs) && typeof existingState.work_item_refs[0] === "string"
|
|
79
|
+
? existingState.work_item_refs[0]
|
|
80
|
+
: "";
|
|
67
81
|
if (providerRef) {
|
|
82
|
+
if (existingRef && existingRef !== providerRef) {
|
|
83
|
+
die(`ensure-session refused: session ${JSON.stringify(slug)} is already bound to Work Item ${JSON.stringify(existingRef)}, not ${JSON.stringify(providerRef)}`);
|
|
84
|
+
}
|
|
68
85
|
return { ref: providerRef };
|
|
69
86
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (Array.isArray(existingState.work_item_refs) && typeof existingState.work_item_refs[0] === "string") {
|
|
73
|
-
return { ref: existingState.work_item_refs[0] };
|
|
87
|
+
if (existingRef) {
|
|
88
|
+
return { ref: existingRef };
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
const title = opt(p, "title", slug).trim() || slug;
|
|
@@ -1442,6 +1457,15 @@ function sessionDirFor(root: string, slug: string): string {
|
|
|
1442
1457
|
return dir;
|
|
1443
1458
|
}
|
|
1444
1459
|
|
|
1460
|
+
function assertSafeSessionDirectory(root: string, dir: string): void {
|
|
1461
|
+
if (!fs.existsSync(dir)) return;
|
|
1462
|
+
const stat = fs.lstatSync(dir);
|
|
1463
|
+
const expected = path.join(fs.realpathSync(root), path.basename(dir));
|
|
1464
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(dir) !== expected) {
|
|
1465
|
+
die(`session directory must be a real directory under the artifact root: ${dir}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1445
1469
|
function validateAgentId(agent: string): string {
|
|
1446
1470
|
if (!agent) die("--agent-id is required");
|
|
1447
1471
|
if (path.isAbsolute(agent)) die("--agent-id must be a relative slug");
|
|
@@ -1481,17 +1505,20 @@ function findRepoRootFromDirStrict(startDir: string): string | null {
|
|
|
1481
1505
|
* Find the repository root by walking upward from a starting directory to locate
|
|
1482
1506
|
* the nearest ancestor containing a kits/ subdirectory. Mirrors flow-resolver.ts
|
|
1483
1507
|
* findRepoRoot, but callable from workflow-sidecar.ts without re-importing the
|
|
1484
|
-
* internal helper.
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1508
|
+
* internal helper. A canonical `.kontourai` path falls back to its owning project,
|
|
1509
|
+
* allowing installed package assets to resolve independently of ambient cwd. Other
|
|
1510
|
+
* layouts retain the cwd fallback used by standalone primitive sessions.
|
|
1487
1511
|
*
|
|
1488
|
-
* Do NOT use this
|
|
1489
|
-
*
|
|
1512
|
+
* Do NOT use this permissive variant for publishDelivery's repo-root resolution —
|
|
1513
|
+
* use findRepoRootFromDirStrict there instead, so a scratch/test
|
|
1490
1514
|
* session dir with no repo ancestor fails closed (skips publish) rather than
|
|
1491
1515
|
* silently trusting process.cwd(), which could be an unrelated real repo.
|
|
1492
1516
|
*/
|
|
1493
1517
|
function findRepoRootFromDir(startDir: string): string {
|
|
1494
|
-
|
|
1518
|
+
const discovered = findRepoRootFromDirStrict(startDir);
|
|
1519
|
+
if (discovered) return discovered;
|
|
1520
|
+
if (path.basename(startDir) === ".kontourai") return path.dirname(startDir);
|
|
1521
|
+
return process.cwd();
|
|
1495
1522
|
}
|
|
1496
1523
|
|
|
1497
1524
|
/**
|
|
@@ -1654,6 +1681,11 @@ function currentDir(root: string, actorKey?: string): string | null {
|
|
|
1654
1681
|
}
|
|
1655
1682
|
return dir;
|
|
1656
1683
|
}
|
|
1684
|
+
|
|
1685
|
+
export function currentWorkflowSessionDir(root: string): string | null {
|
|
1686
|
+
const resolved = loadActorIdentityHelper().resolveActor(process.env);
|
|
1687
|
+
return currentDir(path.resolve(root), loadActorIdentityHelper().isUnresolvedActor(resolved.actor) ? undefined : resolved.actor);
|
|
1688
|
+
}
|
|
1657
1689
|
/**
|
|
1658
1690
|
* #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
|
|
1659
1691
|
* exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
|
|
@@ -1786,10 +1818,11 @@ function enforceEnsureSessionOwnership(
|
|
|
1786
1818
|
slug: string,
|
|
1787
1819
|
dir: string,
|
|
1788
1820
|
resolution: { actorStruct: ActorStruct; actorKey: string; branchActorKey: string; unresolved: boolean },
|
|
1789
|
-
|
|
1821
|
+
workItemRef?: string,
|
|
1822
|
+
): { assignmentFile: string; actorKey: string } | null {
|
|
1790
1823
|
if (p.flags.has("skip-ownership-guard")) {
|
|
1791
1824
|
process.stderr.write("[ensure-session] ownership guard skipped via --skip-ownership-guard\n");
|
|
1792
|
-
return;
|
|
1825
|
+
return null;
|
|
1793
1826
|
}
|
|
1794
1827
|
// Design Decision 4 (unchanged from resolveSessionBranch): actor-resolution ambiguity never
|
|
1795
1828
|
// hard-fails session creation. Without a resolvable actor there is no safe identity to claim
|
|
@@ -1797,7 +1830,7 @@ function enforceEnsureSessionOwnership(
|
|
|
1797
1830
|
// than claiming under a synthetic/unstable identity.
|
|
1798
1831
|
if (resolution.unresolved) {
|
|
1799
1832
|
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");
|
|
1800
|
-
return;
|
|
1833
|
+
return null;
|
|
1801
1834
|
}
|
|
1802
1835
|
|
|
1803
1836
|
// F5 fix (fix-plan iteration 1, LOW): match assignment-provider.ts's sanitizeDisplayField
|
|
@@ -1849,6 +1882,9 @@ function enforceEnsureSessionOwnership(
|
|
|
1849
1882
|
// `assignment-provider status --self-actor <branchActorKey>` run by a DIFFERENT tool
|
|
1850
1883
|
// afterward would never recognize this guard's own claim as self.
|
|
1851
1884
|
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1885
|
+
if (workItemRef && assignment.record?.work_item_ref && assignment.record.work_item_ref !== workItemRef) {
|
|
1886
|
+
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)}`);
|
|
1887
|
+
}
|
|
1852
1888
|
const events = readLivenessEvents(root);
|
|
1853
1889
|
const freshList = loadLivenessReadHelper().freshHolders(events, slug, resolution.branchActorKey, nowMs);
|
|
1854
1890
|
effective = computeEffectiveState(assignment, freshList, resolution.branchActorKey, nowMs);
|
|
@@ -1859,9 +1895,28 @@ function enforceEnsureSessionOwnership(
|
|
|
1859
1895
|
// neither applies, this is a documented scope boundary (today's pre-#291 baseline behavior),
|
|
1860
1896
|
// never a silent hole.
|
|
1861
1897
|
process.stderr.write(`[ensure-session] ownership guard not evaluated: provider "${sanitize(assignmentProviderKind)}" requires --effective-state-json\n`);
|
|
1862
|
-
return;
|
|
1898
|
+
return null;
|
|
1863
1899
|
}
|
|
1864
1900
|
|
|
1901
|
+
const selectedWorkEvidence = (): { assignmentFile: string; actorKey: string } | null => {
|
|
1902
|
+
// Only a claim read from the local provider is durable evidence. Precomputed provider state
|
|
1903
|
+
// can authorize entry, but it cannot prove that this process acquired the Work Item.
|
|
1904
|
+
if (!workItemRef || effectiveStateJsonFlag || assignmentProviderKind !== "local-file") return null;
|
|
1905
|
+
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1906
|
+
const record = assignment.record;
|
|
1907
|
+
if (!record
|
|
1908
|
+
|| record.status !== "claimed"
|
|
1909
|
+
|| record.subject_id !== slug
|
|
1910
|
+
|| record.actor_key !== resolution.branchActorKey
|
|
1911
|
+
|| record.work_item_ref !== workItemRef) {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
assignmentFile: assignmentFilePath(root, slug),
|
|
1916
|
+
actorKey: resolution.branchActorKey,
|
|
1917
|
+
};
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1865
1920
|
const resolveBranchForClaim = (): string => {
|
|
1866
1921
|
const existingBranch = fs.existsSync(path.join(dir, "state.json")) ? (loadJson(path.join(dir, "state.json")).branch as string | undefined) : undefined;
|
|
1867
1922
|
return existingBranch || resolveSessionBranch(p, slug);
|
|
@@ -1874,17 +1929,17 @@ function enforceEnsureSessionOwnership(
|
|
|
1874
1929
|
// ActorStruct triple), so this redundant belt-and-suspenders check agrees with the
|
|
1875
1930
|
// effective_state computation instead of silently using a different identity.
|
|
1876
1931
|
const isSelf = effective.reason === "self_is_holder" || (!!effective.holder?.actor && effective.holder.actor === resolution.branchActorKey);
|
|
1877
|
-
if (isSelf) return;
|
|
1932
|
+
if (isSelf) return selectedWorkEvidence();
|
|
1878
1933
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1879
1934
|
const lastAtSuffix = effective.holder?.last_at ? ` (last_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1880
1935
|
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.`);
|
|
1881
|
-
return;
|
|
1936
|
+
return null;
|
|
1882
1937
|
}
|
|
1883
1938
|
case "human-held": {
|
|
1884
1939
|
const assignee = effective.holder?.assignee ? sanitize(effective.holder.assignee) : "an assigned human";
|
|
1885
1940
|
const idleSuffix = effective.holder?.idle_days != null ? ` (idle ${Number(effective.holder.idle_days)} day(s))` : "";
|
|
1886
1941
|
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.`);
|
|
1887
|
-
return;
|
|
1942
|
+
return null;
|
|
1888
1943
|
}
|
|
1889
1944
|
case "reclaimable": {
|
|
1890
1945
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
@@ -1917,11 +1972,12 @@ function enforceEnsureSessionOwnership(
|
|
|
1917
1972
|
// computeEffectiveState's holderActorKey (assignment-provider.ts) matches this same
|
|
1918
1973
|
// branchActorKey string on the next status/guard check, cross-tool.
|
|
1919
1974
|
actorKey: resolution.branchActorKey,
|
|
1975
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1920
1976
|
});
|
|
1921
1977
|
// Render-don't-execute: emit the incumbent's branch so the skill continues it (never a new
|
|
1922
1978
|
// branch). The successor re-enters the SAME artifact dir (deterministic slug) by construction.
|
|
1923
1979
|
printJson({ role: "SupersedeTakeover", subject: sanitize(slug), superseded_actor: holderActor, ...(incumbentBranch ? { resumed_branch: sanitizeWide(incumbentBranch) } : {}), reason: sanitizeWide(takeoverReason) });
|
|
1924
|
-
return;
|
|
1980
|
+
return selectedWorkEvidence();
|
|
1925
1981
|
}
|
|
1926
1982
|
case "free": {
|
|
1927
1983
|
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
@@ -1931,8 +1987,9 @@ function enforceEnsureSessionOwnership(
|
|
|
1931
1987
|
reason: opt(p, "reason", "ensure-session entry"),
|
|
1932
1988
|
// F1 fix (fix-plan iteration 1, HIGH): see the performLocalSupersede call above.
|
|
1933
1989
|
actorKey: resolution.branchActorKey,
|
|
1990
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1934
1991
|
});
|
|
1935
|
-
return;
|
|
1992
|
+
return selectedWorkEvidence();
|
|
1936
1993
|
}
|
|
1937
1994
|
default:
|
|
1938
1995
|
die(`ensure-session ownership guard: unrecognized effective_state ${JSON.stringify(effective.effective_state)}`);
|
|
@@ -2011,6 +2068,7 @@ function preflightEnsureSession(p: ReturnType<typeof parseArgs>): void {
|
|
|
2011
2068
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2012
2069
|
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)"));
|
|
2013
2070
|
const dir = sessionDirFor(root, slug);
|
|
2071
|
+
assertSafeSessionDirectory(root, dir);
|
|
2014
2072
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2015
2073
|
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
2016
2074
|
sessionWorkItem(p, slug, dir);
|
|
@@ -2020,6 +2078,7 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2020
2078
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2021
2079
|
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)"));
|
|
2022
2080
|
const dir = sessionDirFor(root, slug);
|
|
2081
|
+
assertSafeSessionDirectory(root, dir);
|
|
2023
2082
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2024
2083
|
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
2025
2084
|
const workItem = sessionWorkItem(p, slug, dir);
|
|
@@ -2028,8 +2087,11 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2028
2087
|
// below (writeCurrent's per-actor dual-write) so the branch-naming actor and the
|
|
2029
2088
|
// assignment-claim actor are always the same identity.
|
|
2030
2089
|
const actorResolution = resolveEnsureSessionActor(p);
|
|
2031
|
-
|
|
2032
|
-
|
|
2090
|
+
const selectionWorkItemRef = entry.flowId === "builder.build" && assignmentSubjectMatchesWorkItem(slug, workItem.ref)
|
|
2091
|
+
? workItem.ref
|
|
2092
|
+
: undefined;
|
|
2093
|
+
const selectedWorkEvidence = enforceEnsureSessionOwnership(p, root, slug, dir, actorResolution, selectionWorkItemRef);
|
|
2094
|
+
ensureSafeDirectory(root, dir);
|
|
2033
2095
|
const timestamp = opt(p, "timestamp", now());
|
|
2034
2096
|
if (workItem.localRecord && !fs.existsSync(path.join(dir, "work-item.json"))) {
|
|
2035
2097
|
writeJson(path.join(dir, "work-item.json"), workItem.localRecord);
|
|
@@ -2042,13 +2104,24 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2042
2104
|
// takeover continuity true by construction (see Design Decision 3 in the plan).
|
|
2043
2105
|
const branch = resolveSessionBranch(p, slug);
|
|
2044
2106
|
const initialMarkdownStatus = entry.flowId ? "new" : "planning";
|
|
2045
|
-
|
|
2107
|
+
const acceptanceCriteria = opts(p, "criterion");
|
|
2108
|
+
if (acceptanceCriteria.length === 0) acceptanceCriteria.push(`Complete the requested outcome: ${opt(p, "summary", "Workflow session is durable.")}`);
|
|
2109
|
+
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`;
|
|
2046
2110
|
fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), md);
|
|
2047
2111
|
}
|
|
2048
2112
|
if (!fs.existsSync(path.join(dir, "state.json")) || !fs.existsSync(path.join(dir, "acceptance.json")) || !fs.existsSync(path.join(dir, "handoff.json"))) {
|
|
2049
2113
|
const phaseMap = entry.flowId ? resolvePhaseMap(entry.flowId, findRepoRootFromDir(dir)) : null;
|
|
2050
2114
|
const initialPhase = Object.entries(phaseMap ?? {}).find(([, step]) => step === entry.firstStep)?.[0];
|
|
2051
|
-
const startCommand =
|
|
2115
|
+
const startCommand = pinnedFlowAgentsCommand(flowAgentsPackageVersion(), [
|
|
2116
|
+
"workflow", "start",
|
|
2117
|
+
"--flow", "builder.build",
|
|
2118
|
+
"--work-item", workItem.ref,
|
|
2119
|
+
...(workItem.ref === `local:${slug}` ? ["--task-slug", slug] : []),
|
|
2120
|
+
"--artifact-root", root,
|
|
2121
|
+
"--title", opt(p, "title", slug),
|
|
2122
|
+
"--summary", opt(p, "summary", "Workflow session is durable."),
|
|
2123
|
+
...opts(p, "criterion").flatMap((criterion) => ["--criterion", criterion]),
|
|
2124
|
+
]);
|
|
2052
2125
|
const nextAction: string | AnyObj = entry.flowId
|
|
2053
2126
|
? entry.flowId === "builder.build"
|
|
2054
2127
|
? {
|
|
@@ -2088,10 +2161,41 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2088
2161
|
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", entry.flowId || undefined, resumedStep || undefined, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
2089
2162
|
if (entry.flowId === "builder.build") {
|
|
2090
2163
|
try {
|
|
2091
|
-
await startBuilderFlowSession({ sessionDir: dir });
|
|
2164
|
+
const started = await startBuilderFlowSession({ sessionDir: dir });
|
|
2165
|
+
if (started.run.state.current_step === "pull-work"
|
|
2166
|
+
&& selectedWorkEvidence
|
|
2167
|
+
&& assignmentSubjectMatchesWorkItem(slug, workItem.ref)) {
|
|
2168
|
+
await withSubjectLock(root, slug, async () => {
|
|
2169
|
+
const assignment = readLocalAssignmentStatus(root, slug).record;
|
|
2170
|
+
if (!assignment
|
|
2171
|
+
|| assignment.status !== "claimed"
|
|
2172
|
+
|| assignment.subject_id !== slug
|
|
2173
|
+
|| assignment.actor_key !== selectedWorkEvidence.actorKey
|
|
2174
|
+
|| assignment.work_item_ref !== workItem.ref) {
|
|
2175
|
+
die(`ensure-session refused to emit selected-work evidence: assignment provenance changed before gate evaluation for ${JSON.stringify(workItem.ref)}`);
|
|
2176
|
+
}
|
|
2177
|
+
const assignmentContent = fs.readFileSync(selectedWorkEvidence.assignmentFile);
|
|
2178
|
+
const assignmentDigest = createHash("sha256").update(assignmentContent).digest("hex");
|
|
2179
|
+
const projectRoot = path.dirname(path.dirname(root));
|
|
2180
|
+
const evidenceFile = path.relative(projectRoot, selectedWorkEvidence.assignmentFile);
|
|
2181
|
+
await recordGateClaim(parseArgs([
|
|
2182
|
+
"record-gate-claim",
|
|
2183
|
+
dir,
|
|
2184
|
+
"--actor", selectedWorkEvidence.actorKey,
|
|
2185
|
+
"--expectation", "selected-work",
|
|
2186
|
+
"--status", "pass",
|
|
2187
|
+
"--summary", `Work Item ${workItem.ref} is durably assigned to the active workflow actor (assignment sha256:${assignmentDigest}).`,
|
|
2188
|
+
"--evidence-ref-json", JSON.stringify({
|
|
2189
|
+
kind: "artifact",
|
|
2190
|
+
file: evidenceFile,
|
|
2191
|
+
summary: `Durable local assignment record for the selected Work Item and active actor; sha256:${assignmentDigest}.`,
|
|
2192
|
+
}),
|
|
2193
|
+
"--timestamp", timestamp,
|
|
2194
|
+
]));
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2092
2197
|
} catch (error) {
|
|
2093
|
-
|
|
2094
|
-
process.stderr.write(`[ensure-session] canonical Builder Flow run did not start; the sidecar remains retryable. Run: ${retry}\n`);
|
|
2198
|
+
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");
|
|
2095
2199
|
throw error;
|
|
2096
2200
|
}
|
|
2097
2201
|
}
|
|
@@ -5336,7 +5440,7 @@ function loadLivenessReadHelper(): {
|
|
|
5336
5440
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
5337
5441
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
5338
5442
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
5339
|
-
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
5443
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "archived"]);
|
|
5340
5444
|
/**
|
|
5341
5445
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
5342
5446
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|
|
@@ -5804,8 +5908,8 @@ Available claim ids:
|
|
|
5804
5908
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5805
5909
|
|
|
5806
5910
|
|
|
5807
|
-
async function main(): Promise<number> {
|
|
5808
|
-
const _rawArgv =
|
|
5911
|
+
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
5912
|
+
const _rawArgv = argv;
|
|
5809
5913
|
// #380: `record-check <dir> -- <command...>` — argv after the FIRST literal `--` token is the
|
|
5810
5914
|
// command to execute verbatim (never option-parsed: a command like `npm test -- --watch`
|
|
5811
5915
|
// legitimately contains its OWN `--`, so only the record-check dispatcher's own separator, the
|
|
@@ -5880,5 +5984,8 @@ async function main(): Promise<number> {
|
|
|
5880
5984
|
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
5881
5985
|
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
5882
5986
|
if (_selfRealPath === _argv1RealPath) {
|
|
5987
|
+
if (path.basename(process.argv[1] ?? "") === "flow-agents-workflow-sidecar") {
|
|
5988
|
+
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");
|
|
5989
|
+
}
|
|
5883
5990
|
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
5884
5991
|
}
|