@kontourai/flow-agents 3.4.3 → 3.5.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/CHANGELOG.md +7 -0
- package/build/src/cli/assignment-provider.d.ts +3 -0
- package/build/src/cli/assignment-provider.js +59 -10
- package/build/src/cli/workflow-sidecar.js +112 -24
- package/build/src/lib/flow-resolver.js +7 -2
- package/context/contracts/assignment-provider-contract.md +5 -2
- package/docs/spec/builder-flow-runtime.md +12 -5
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +236 -20
- package/evals/integration/test_bundle_install.sh +43 -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_workflow_sidecar_writer.sh +1 -0
- package/package.json +1 -1
- package/src/cli/assignment-provider.ts +55 -11
- package/src/cli/workflow-sidecar.ts +110 -25
- package/src/lib/flow-resolver.ts +6 -2
|
@@ -9,12 +9,13 @@ 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";
|
|
12
13
|
import { startBuilderFlowSession, syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
|
|
13
14
|
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
14
15
|
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
15
16
|
// `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
|
|
16
17
|
// 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";
|
|
18
|
+
import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus, withSubjectLock, type ActorStruct, type EffectiveState, type FreshHolder } from "./assignment-provider.js";
|
|
18
19
|
|
|
19
20
|
type AnyObj = Record<string, any>;
|
|
20
21
|
|
|
@@ -57,6 +58,12 @@ function workItemSlug(ref: string): string {
|
|
|
57
58
|
return slugify(`${owner}-${repo}-${id}`, "work-item");
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
function assignmentSubjectMatchesWorkItem(slug: string, ref: string): boolean {
|
|
62
|
+
if (ref === `local:${slug}`) return true;
|
|
63
|
+
if (!/^[^/#]+\/[^/#]+#\d+$/.test(ref)) return false;
|
|
64
|
+
return workItemSlug(ref) === slug;
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
type SessionWorkItem = {
|
|
61
68
|
ref: string;
|
|
62
69
|
localRecord?: AnyObj;
|
|
@@ -64,13 +71,18 @@ type SessionWorkItem = {
|
|
|
64
71
|
|
|
65
72
|
function sessionWorkItem(p: ReturnType<typeof parseArgs>, slug: string, dir: string): SessionWorkItem {
|
|
66
73
|
const providerRef = opt(p, "work-item");
|
|
74
|
+
const existingState = loadJson(path.join(dir, "state.json"));
|
|
75
|
+
const existingRef = Array.isArray(existingState.work_item_refs) && typeof existingState.work_item_refs[0] === "string"
|
|
76
|
+
? existingState.work_item_refs[0]
|
|
77
|
+
: "";
|
|
67
78
|
if (providerRef) {
|
|
79
|
+
if (existingRef && existingRef !== providerRef) {
|
|
80
|
+
die(`ensure-session refused: session ${JSON.stringify(slug)} is already bound to Work Item ${JSON.stringify(existingRef)}, not ${JSON.stringify(providerRef)}`);
|
|
81
|
+
}
|
|
68
82
|
return { ref: providerRef };
|
|
69
83
|
}
|
|
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] };
|
|
84
|
+
if (existingRef) {
|
|
85
|
+
return { ref: existingRef };
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
const title = opt(p, "title", slug).trim() || slug;
|
|
@@ -1442,6 +1454,15 @@ function sessionDirFor(root: string, slug: string): string {
|
|
|
1442
1454
|
return dir;
|
|
1443
1455
|
}
|
|
1444
1456
|
|
|
1457
|
+
function assertSafeSessionDirectory(root: string, dir: string): void {
|
|
1458
|
+
if (!fs.existsSync(dir)) return;
|
|
1459
|
+
const stat = fs.lstatSync(dir);
|
|
1460
|
+
const expected = path.join(fs.realpathSync(root), path.basename(dir));
|
|
1461
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(dir) !== expected) {
|
|
1462
|
+
die(`session directory must be a real directory under the artifact root: ${dir}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1445
1466
|
function validateAgentId(agent: string): string {
|
|
1446
1467
|
if (!agent) die("--agent-id is required");
|
|
1447
1468
|
if (path.isAbsolute(agent)) die("--agent-id must be a relative slug");
|
|
@@ -1481,17 +1502,20 @@ function findRepoRootFromDirStrict(startDir: string): string | null {
|
|
|
1481
1502
|
* Find the repository root by walking upward from a starting directory to locate
|
|
1482
1503
|
* the nearest ancestor containing a kits/ subdirectory. Mirrors flow-resolver.ts
|
|
1483
1504
|
* findRepoRoot, but callable from workflow-sidecar.ts without re-importing the
|
|
1484
|
-
* internal helper.
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1505
|
+
* internal helper. A canonical `.kontourai` path falls back to its owning project,
|
|
1506
|
+
* allowing installed package assets to resolve independently of ambient cwd. Other
|
|
1507
|
+
* layouts retain the cwd fallback used by standalone primitive sessions.
|
|
1487
1508
|
*
|
|
1488
|
-
* Do NOT use this
|
|
1489
|
-
*
|
|
1509
|
+
* Do NOT use this permissive variant for publishDelivery's repo-root resolution —
|
|
1510
|
+
* use findRepoRootFromDirStrict there instead, so a scratch/test
|
|
1490
1511
|
* session dir with no repo ancestor fails closed (skips publish) rather than
|
|
1491
1512
|
* silently trusting process.cwd(), which could be an unrelated real repo.
|
|
1492
1513
|
*/
|
|
1493
1514
|
function findRepoRootFromDir(startDir: string): string {
|
|
1494
|
-
|
|
1515
|
+
const discovered = findRepoRootFromDirStrict(startDir);
|
|
1516
|
+
if (discovered) return discovered;
|
|
1517
|
+
if (path.basename(startDir) === ".kontourai") return path.dirname(startDir);
|
|
1518
|
+
return process.cwd();
|
|
1495
1519
|
}
|
|
1496
1520
|
|
|
1497
1521
|
/**
|
|
@@ -1786,10 +1810,11 @@ function enforceEnsureSessionOwnership(
|
|
|
1786
1810
|
slug: string,
|
|
1787
1811
|
dir: string,
|
|
1788
1812
|
resolution: { actorStruct: ActorStruct; actorKey: string; branchActorKey: string; unresolved: boolean },
|
|
1789
|
-
|
|
1813
|
+
workItemRef?: string,
|
|
1814
|
+
): { assignmentFile: string; actorKey: string } | null {
|
|
1790
1815
|
if (p.flags.has("skip-ownership-guard")) {
|
|
1791
1816
|
process.stderr.write("[ensure-session] ownership guard skipped via --skip-ownership-guard\n");
|
|
1792
|
-
return;
|
|
1817
|
+
return null;
|
|
1793
1818
|
}
|
|
1794
1819
|
// Design Decision 4 (unchanged from resolveSessionBranch): actor-resolution ambiguity never
|
|
1795
1820
|
// hard-fails session creation. Without a resolvable actor there is no safe identity to claim
|
|
@@ -1797,7 +1822,7 @@ function enforceEnsureSessionOwnership(
|
|
|
1797
1822
|
// than claiming under a synthetic/unstable identity.
|
|
1798
1823
|
if (resolution.unresolved) {
|
|
1799
1824
|
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;
|
|
1825
|
+
return null;
|
|
1801
1826
|
}
|
|
1802
1827
|
|
|
1803
1828
|
// F5 fix (fix-plan iteration 1, LOW): match assignment-provider.ts's sanitizeDisplayField
|
|
@@ -1849,6 +1874,9 @@ function enforceEnsureSessionOwnership(
|
|
|
1849
1874
|
// `assignment-provider status --self-actor <branchActorKey>` run by a DIFFERENT tool
|
|
1850
1875
|
// afterward would never recognize this guard's own claim as self.
|
|
1851
1876
|
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1877
|
+
if (workItemRef && assignment.record?.work_item_ref && assignment.record.work_item_ref !== workItemRef) {
|
|
1878
|
+
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)}`);
|
|
1879
|
+
}
|
|
1852
1880
|
const events = readLivenessEvents(root);
|
|
1853
1881
|
const freshList = loadLivenessReadHelper().freshHolders(events, slug, resolution.branchActorKey, nowMs);
|
|
1854
1882
|
effective = computeEffectiveState(assignment, freshList, resolution.branchActorKey, nowMs);
|
|
@@ -1859,9 +1887,28 @@ function enforceEnsureSessionOwnership(
|
|
|
1859
1887
|
// neither applies, this is a documented scope boundary (today's pre-#291 baseline behavior),
|
|
1860
1888
|
// never a silent hole.
|
|
1861
1889
|
process.stderr.write(`[ensure-session] ownership guard not evaluated: provider "${sanitize(assignmentProviderKind)}" requires --effective-state-json\n`);
|
|
1862
|
-
return;
|
|
1890
|
+
return null;
|
|
1863
1891
|
}
|
|
1864
1892
|
|
|
1893
|
+
const selectedWorkEvidence = (): { assignmentFile: string; actorKey: string } | null => {
|
|
1894
|
+
// Only a claim read from the local provider is durable evidence. Precomputed provider state
|
|
1895
|
+
// can authorize entry, but it cannot prove that this process acquired the Work Item.
|
|
1896
|
+
if (!workItemRef || effectiveStateJsonFlag || assignmentProviderKind !== "local-file") return null;
|
|
1897
|
+
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1898
|
+
const record = assignment.record;
|
|
1899
|
+
if (!record
|
|
1900
|
+
|| record.status !== "claimed"
|
|
1901
|
+
|| record.subject_id !== slug
|
|
1902
|
+
|| record.actor_key !== resolution.branchActorKey
|
|
1903
|
+
|| record.work_item_ref !== workItemRef) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
assignmentFile: assignmentFilePath(root, slug),
|
|
1908
|
+
actorKey: resolution.branchActorKey,
|
|
1909
|
+
};
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1865
1912
|
const resolveBranchForClaim = (): string => {
|
|
1866
1913
|
const existingBranch = fs.existsSync(path.join(dir, "state.json")) ? (loadJson(path.join(dir, "state.json")).branch as string | undefined) : undefined;
|
|
1867
1914
|
return existingBranch || resolveSessionBranch(p, slug);
|
|
@@ -1874,17 +1921,17 @@ function enforceEnsureSessionOwnership(
|
|
|
1874
1921
|
// ActorStruct triple), so this redundant belt-and-suspenders check agrees with the
|
|
1875
1922
|
// effective_state computation instead of silently using a different identity.
|
|
1876
1923
|
const isSelf = effective.reason === "self_is_holder" || (!!effective.holder?.actor && effective.holder.actor === resolution.branchActorKey);
|
|
1877
|
-
if (isSelf) return;
|
|
1924
|
+
if (isSelf) return selectedWorkEvidence();
|
|
1878
1925
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1879
1926
|
const lastAtSuffix = effective.holder?.last_at ? ` (last_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1880
1927
|
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;
|
|
1928
|
+
return null;
|
|
1882
1929
|
}
|
|
1883
1930
|
case "human-held": {
|
|
1884
1931
|
const assignee = effective.holder?.assignee ? sanitize(effective.holder.assignee) : "an assigned human";
|
|
1885
1932
|
const idleSuffix = effective.holder?.idle_days != null ? ` (idle ${Number(effective.holder.idle_days)} day(s))` : "";
|
|
1886
1933
|
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;
|
|
1934
|
+
return null;
|
|
1888
1935
|
}
|
|
1889
1936
|
case "reclaimable": {
|
|
1890
1937
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
@@ -1917,11 +1964,12 @@ function enforceEnsureSessionOwnership(
|
|
|
1917
1964
|
// computeEffectiveState's holderActorKey (assignment-provider.ts) matches this same
|
|
1918
1965
|
// branchActorKey string on the next status/guard check, cross-tool.
|
|
1919
1966
|
actorKey: resolution.branchActorKey,
|
|
1967
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1920
1968
|
});
|
|
1921
1969
|
// Render-don't-execute: emit the incumbent's branch so the skill continues it (never a new
|
|
1922
1970
|
// branch). The successor re-enters the SAME artifact dir (deterministic slug) by construction.
|
|
1923
1971
|
printJson({ role: "SupersedeTakeover", subject: sanitize(slug), superseded_actor: holderActor, ...(incumbentBranch ? { resumed_branch: sanitizeWide(incumbentBranch) } : {}), reason: sanitizeWide(takeoverReason) });
|
|
1924
|
-
return;
|
|
1972
|
+
return selectedWorkEvidence();
|
|
1925
1973
|
}
|
|
1926
1974
|
case "free": {
|
|
1927
1975
|
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
@@ -1931,8 +1979,9 @@ function enforceEnsureSessionOwnership(
|
|
|
1931
1979
|
reason: opt(p, "reason", "ensure-session entry"),
|
|
1932
1980
|
// F1 fix (fix-plan iteration 1, HIGH): see the performLocalSupersede call above.
|
|
1933
1981
|
actorKey: resolution.branchActorKey,
|
|
1982
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
1934
1983
|
});
|
|
1935
|
-
return;
|
|
1984
|
+
return selectedWorkEvidence();
|
|
1936
1985
|
}
|
|
1937
1986
|
default:
|
|
1938
1987
|
die(`ensure-session ownership guard: unrecognized effective_state ${JSON.stringify(effective.effective_state)}`);
|
|
@@ -2011,6 +2060,7 @@ function preflightEnsureSession(p: ReturnType<typeof parseArgs>): void {
|
|
|
2011
2060
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2012
2061
|
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
2062
|
const dir = sessionDirFor(root, slug);
|
|
2063
|
+
assertSafeSessionDirectory(root, dir);
|
|
2014
2064
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2015
2065
|
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
2016
2066
|
sessionWorkItem(p, slug, dir);
|
|
@@ -2020,6 +2070,7 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2020
2070
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2021
2071
|
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
2072
|
const dir = sessionDirFor(root, slug);
|
|
2073
|
+
assertSafeSessionDirectory(root, dir);
|
|
2023
2074
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2024
2075
|
if (entry.flowId === "builder.build") assertCanonicalBuilderArtifactRoot(root);
|
|
2025
2076
|
const workItem = sessionWorkItem(p, slug, dir);
|
|
@@ -2028,8 +2079,11 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2028
2079
|
// below (writeCurrent's per-actor dual-write) so the branch-naming actor and the
|
|
2029
2080
|
// assignment-claim actor are always the same identity.
|
|
2030
2081
|
const actorResolution = resolveEnsureSessionActor(p);
|
|
2031
|
-
|
|
2032
|
-
|
|
2082
|
+
const selectionWorkItemRef = entry.flowId === "builder.build" && assignmentSubjectMatchesWorkItem(slug, workItem.ref)
|
|
2083
|
+
? workItem.ref
|
|
2084
|
+
: undefined;
|
|
2085
|
+
const selectedWorkEvidence = enforceEnsureSessionOwnership(p, root, slug, dir, actorResolution, selectionWorkItemRef);
|
|
2086
|
+
ensureSafeDirectory(root, dir);
|
|
2033
2087
|
const timestamp = opt(p, "timestamp", now());
|
|
2034
2088
|
if (workItem.localRecord && !fs.existsSync(path.join(dir, "work-item.json"))) {
|
|
2035
2089
|
writeJson(path.join(dir, "work-item.json"), workItem.localRecord);
|
|
@@ -2088,10 +2142,41 @@ async function ensureSession(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2088
2142
|
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", entry.flowId || undefined, resumedStep || undefined, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
2089
2143
|
if (entry.flowId === "builder.build") {
|
|
2090
2144
|
try {
|
|
2091
|
-
await startBuilderFlowSession({ sessionDir: dir });
|
|
2145
|
+
const started = await startBuilderFlowSession({ sessionDir: dir });
|
|
2146
|
+
if (started.run.state.current_step === "pull-work"
|
|
2147
|
+
&& selectedWorkEvidence
|
|
2148
|
+
&& assignmentSubjectMatchesWorkItem(slug, workItem.ref)) {
|
|
2149
|
+
await withSubjectLock(root, slug, async () => {
|
|
2150
|
+
const assignment = readLocalAssignmentStatus(root, slug).record;
|
|
2151
|
+
if (!assignment
|
|
2152
|
+
|| assignment.status !== "claimed"
|
|
2153
|
+
|| assignment.subject_id !== slug
|
|
2154
|
+
|| assignment.actor_key !== selectedWorkEvidence.actorKey
|
|
2155
|
+
|| assignment.work_item_ref !== workItem.ref) {
|
|
2156
|
+
die(`ensure-session refused to emit selected-work evidence: assignment provenance changed before gate evaluation for ${JSON.stringify(workItem.ref)}`);
|
|
2157
|
+
}
|
|
2158
|
+
const assignmentContent = fs.readFileSync(selectedWorkEvidence.assignmentFile);
|
|
2159
|
+
const assignmentDigest = createHash("sha256").update(assignmentContent).digest("hex");
|
|
2160
|
+
const projectRoot = path.dirname(path.dirname(root));
|
|
2161
|
+
const evidenceFile = path.relative(projectRoot, selectedWorkEvidence.assignmentFile);
|
|
2162
|
+
await recordGateClaim(parseArgs([
|
|
2163
|
+
"record-gate-claim",
|
|
2164
|
+
dir,
|
|
2165
|
+
"--actor", selectedWorkEvidence.actorKey,
|
|
2166
|
+
"--expectation", "selected-work",
|
|
2167
|
+
"--status", "pass",
|
|
2168
|
+
"--summary", `Work Item ${workItem.ref} is durably assigned to the active workflow actor (assignment sha256:${assignmentDigest}).`,
|
|
2169
|
+
"--evidence-ref-json", JSON.stringify({
|
|
2170
|
+
kind: "artifact",
|
|
2171
|
+
file: evidenceFile,
|
|
2172
|
+
summary: `Durable local assignment record for the selected Work Item and active actor; sha256:${assignmentDigest}.`,
|
|
2173
|
+
}),
|
|
2174
|
+
"--timestamp", timestamp,
|
|
2175
|
+
]));
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2092
2178
|
} catch (error) {
|
|
2093
|
-
|
|
2094
|
-
process.stderr.write(`[ensure-session] canonical Builder Flow run did not start; the sidecar remains retryable. Run: ${retry}\n`);
|
|
2179
|
+
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
2180
|
throw error;
|
|
2096
2181
|
}
|
|
2097
2182
|
}
|
package/src/lib/flow-resolver.ts
CHANGED
|
@@ -449,8 +449,8 @@ export function resolvePhaseMap(flowId: string, repoRoot: string): Record<string
|
|
|
449
449
|
|
|
450
450
|
/**
|
|
451
451
|
* Find the repository root from a starting directory by walking upward to locate
|
|
452
|
-
* the nearest ancestor that contains a `kits/` subdirectory.
|
|
453
|
-
* falls back to
|
|
452
|
+
* the nearest ancestor that contains a `kits/` subdirectory. A canonical `.kontourai`
|
|
453
|
+
* artifact path falls back to its owning project; other layouts fall back to cwd.
|
|
454
454
|
*
|
|
455
455
|
* This is required because the runtime artifact directory can live anywhere (temp dirs,
|
|
456
456
|
* subprojects, CI workspaces) while the kits/ directory is always at the repo root.
|
|
@@ -464,6 +464,10 @@ function findRepoRoot(startDir: string): string {
|
|
|
464
464
|
if (parent === dir) break; // reached filesystem root
|
|
465
465
|
dir = parent;
|
|
466
466
|
}
|
|
467
|
+
// A canonical product artifact root identifies its owning project even when that
|
|
468
|
+
// consumer has no source-tree kits/. resolveFlowFilePath can then use the installed
|
|
469
|
+
// package fallback without consulting an unrelated ambient cwd.
|
|
470
|
+
if (path.basename(startDir) === ".kontourai") return path.dirname(startDir);
|
|
467
471
|
// Fallback: process.cwd() covers the common "run from repo root" case
|
|
468
472
|
return process.cwd();
|
|
469
473
|
}
|