@kontourai/flow-agents 2.1.1 → 2.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/CODEOWNERS +2 -0
- package/CHANGELOG.md +22 -0
- package/build/src/cli/sidecar-claim-explain.d.ts +45 -0
- package/build/src/cli/sidecar-claim-explain.js +87 -0
- package/build/src/cli/telemetry-doctor.js +2 -4
- package/build/src/cli/usage-feedback.js +16 -8
- package/build/src/cli/workflow-artifact-cleanup-audit.js +4 -2
- package/build/src/cli/workflow-sidecar.d.ts +2 -44
- package/build/src/cli/workflow-sidecar.js +18 -87
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/local-artifact-root.d.ts +11 -0
- package/build/src/lib/local-artifact-root.js +30 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/context/scripts/telemetry/lib/config.sh +3 -4
- package/docs/adr/0018-freeze-local-shell-heuristics.md +95 -0
- package/docs/repository-structure.md +7 -3
- package/evals/integration/test_bundle_lifecycle.sh +9 -9
- package/evals/integration/test_gate_lockdown.sh +4 -0
- package/evals/integration/test_migrate_local_artifacts.sh +102 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +34 -0
- package/evals/run.sh +2 -0
- package/evals/static/test_unit_helpers.sh +26 -0
- package/integrations/strands-ts/package.json +3 -0
- package/integrations/strands-ts/src/telemetry.ts +31 -50
- package/kits/knowledge/adapters/flow-runner/index.js +1 -1
- package/kits/knowledge/adapters/flow-runner/telemetry.js +2 -2
- package/package.json +2 -1
- package/scripts/README.md +1 -0
- package/scripts/hooks/config-protection.js +6 -0
- package/scripts/hooks/evidence-capture.js +16 -10
- package/scripts/hooks/lib/local-artifact-paths.js +32 -0
- package/scripts/hooks/stop-goal-fit.js +19 -13
- package/scripts/hooks/workflow-steering.js +5 -3
- package/scripts/lib/command-log-chain.js +5 -0
- package/scripts/migrate-local-artifacts.mjs +230 -0
- package/scripts/telemetry/lib/config.sh +3 -4
- package/src/cli/public-api.test.mjs +21 -0
- package/src/cli/sidecar-claim-explain.ts +130 -0
- package/src/cli/sidecar-pure-helpers.test.mjs +168 -0
- package/src/cli/telemetry-doctor.ts +2 -4
- package/src/cli/usage-feedback.ts +15 -8
- package/src/cli/workflow-artifact-cleanup-audit.ts +4 -2
- package/src/cli/workflow-sidecar.ts +17 -131
- package/src/index.ts +14 -0
- package/src/lib/local-artifact-root.ts +39 -0
- package/src/tools/validate-source-tree.ts +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const KONTOURAI_DIR = '.kontourai';
|
|
7
|
+
const LEGACY_FLOW_AGENTS_DIR = '.flow-agents';
|
|
8
|
+
|
|
9
|
+
function flowAgentsArtifactRoot(cwd = process.cwd()) {
|
|
10
|
+
return path.resolve(cwd, KONTOURAI_DIR, 'flow-agents');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function legacyFlowAgentsArtifactRoot(cwd = process.cwd()) {
|
|
14
|
+
return path.resolve(cwd, LEGACY_FLOW_AGENTS_DIR);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function flowAgentsArtifactRootsForRead(cwd = process.cwd()) {
|
|
18
|
+
const roots = [flowAgentsArtifactRoot(cwd), legacyFlowAgentsArtifactRoot(cwd)];
|
|
19
|
+
return roots.filter((root, index) => roots.indexOf(root) === index && fs.existsSync(root));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function defaultArtifactRootForRead(cwd = process.cwd()) {
|
|
23
|
+
const roots = flowAgentsArtifactRootsForRead(cwd);
|
|
24
|
+
return roots[0] || flowAgentsArtifactRoot(cwd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
flowAgentsArtifactRoot,
|
|
29
|
+
legacyFlowAgentsArtifactRoot,
|
|
30
|
+
flowAgentsArtifactRootsForRead,
|
|
31
|
+
defaultArtifactRootForRead,
|
|
32
|
+
};
|
|
@@ -38,6 +38,10 @@ const {
|
|
|
38
38
|
canonicalJsonForChain,
|
|
39
39
|
hasLaunderingOperator,
|
|
40
40
|
} = require('../lib/command-log-chain.js');
|
|
41
|
+
const {
|
|
42
|
+
flowAgentsArtifactRoot,
|
|
43
|
+
flowAgentsArtifactRootsForRead,
|
|
44
|
+
} = require('./lib/local-artifact-paths');
|
|
41
45
|
|
|
42
46
|
const MAX_STDIN = 1024 * 1024;
|
|
43
47
|
const ACTIVE_STATUSES = new Set([
|
|
@@ -1575,11 +1579,11 @@ const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence
|
|
|
1575
1579
|
const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered/;
|
|
1576
1580
|
|
|
1577
1581
|
async function analyze(root, now = Date.now()) {
|
|
1578
|
-
const
|
|
1582
|
+
const flowAgentsDirs = flowAgentsArtifactRootsForRead(root);
|
|
1579
1583
|
// Scope to the session's current task when current.json names one, so an
|
|
1580
1584
|
// unrelated active workflow elsewhere in the repo does not gate this stop.
|
|
1581
|
-
const scoped = preferredArtifactDir(
|
|
1582
|
-
const searchDirs = scoped ? [scoped] :
|
|
1585
|
+
const scoped = flowAgentsDirs.map(preferredArtifactDir).find(Boolean);
|
|
1586
|
+
const searchDirs = scoped ? [scoped] : flowAgentsDirs;
|
|
1583
1587
|
const artifacts = searchDirs
|
|
1584
1588
|
.flatMap(dir => walkMarkdown(dir))
|
|
1585
1589
|
.map(readArtifact)
|
|
@@ -1589,6 +1593,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1589
1593
|
if (artifacts.length === 0) return { warnings: [], blocking: false };
|
|
1590
1594
|
|
|
1591
1595
|
const latest = artifacts[0];
|
|
1596
|
+
const latestArtifactDir = path.dirname(latest.file);
|
|
1592
1597
|
const warnings = [];
|
|
1593
1598
|
const relPath = relative(root, latest.file);
|
|
1594
1599
|
const status = latest.status || 'unknown';
|
|
@@ -1604,11 +1609,11 @@ async function analyze(root, now = Date.now()) {
|
|
|
1604
1609
|
|
|
1605
1610
|
// ADR 0016 P-c: load the active FlowDefinition gate (fail-open: null when absent).
|
|
1606
1611
|
// Null → existing workflow.* fallback path unchanged. Non-null → expects[]-driven claim selection.
|
|
1607
|
-
const activeFlowStep = loadActiveFlowStep(
|
|
1612
|
+
const activeFlowStep = loadActiveFlowStep(path.dirname(latestArtifactDir));
|
|
1608
1613
|
|
|
1609
|
-
warnings.push(...sidecarValidation(root,
|
|
1610
|
-
warnings.push(...sidecarGuidance(root,
|
|
1611
|
-
const captureWarnings = captureCrossReference(root,
|
|
1614
|
+
warnings.push(...sidecarValidation(root, latestArtifactDir));
|
|
1615
|
+
warnings.push(...sidecarGuidance(root, latestArtifactDir, activeFlowStep));
|
|
1616
|
+
const captureWarnings = captureCrossReference(root, latestArtifactDir, activeFlowStep);
|
|
1612
1617
|
warnings.push(...captureWarnings);
|
|
1613
1618
|
// Dedup: bundleEnforcement and captureCrossReference can both fire "caught false-completion"
|
|
1614
1619
|
// for the same disputed claim. Suppress the bundleEnforcement warning ONLY when
|
|
@@ -1622,7 +1627,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1622
1627
|
const m = /evidence check ([^\s:]+):/.exec(w);
|
|
1623
1628
|
if (m) captureHardBlockIds.add(m[1]);
|
|
1624
1629
|
}
|
|
1625
|
-
const bundleWarnings = (await bundleEnforcement(
|
|
1630
|
+
const bundleWarnings = (await bundleEnforcement(latestArtifactDir, activeFlowStep)).filter(w => {
|
|
1626
1631
|
if (!captureHardBlockIds.size) return true;
|
|
1627
1632
|
// bundleEnforcement warns: "trust.bundle claim disputed: <subjectId> ..."
|
|
1628
1633
|
const m = /trust\.bundle claim (?:disputed|tampered): ([^\s(]+)/.exec(w);
|
|
@@ -1634,21 +1639,21 @@ async function analyze(root, now = Date.now()) {
|
|
|
1634
1639
|
return !captureHardBlockIds.has(checkId);
|
|
1635
1640
|
});
|
|
1636
1641
|
warnings.push(...bundleWarnings);
|
|
1637
|
-
warnings.push(...missingBundleOrStateSignal(
|
|
1642
|
+
warnings.push(...missingBundleOrStateSignal(latestArtifactDir, activeFlowStep));
|
|
1638
1643
|
|
|
1639
1644
|
// A pre-execution task (not started) OR a terminal task (which is itself a
|
|
1640
1645
|
// completion *claim*) must not block on mere incompleteness — but a FALSE claim
|
|
1641
1646
|
// (capture/evidence contradiction) still blocks at any phase. This is the whole
|
|
1642
1647
|
// point of the capture cross-reference: catch a task that falsely claims done.
|
|
1643
|
-
const gateState = readJsonFile(path.join(
|
|
1648
|
+
const gateState = readJsonFile(path.join(latestArtifactDir, 'state.json'));
|
|
1644
1649
|
const taskStatus = gateState ? normalizedStatus(gateState.status) : normalizedStatus(status);
|
|
1645
|
-
const preExecution = isPreExecution(
|
|
1650
|
+
const preExecution = isPreExecution(latestArtifactDir, status);
|
|
1646
1651
|
const terminal = TERMINAL_STATUSES.has(taskStatus);
|
|
1647
1652
|
|
|
1648
1653
|
// Namespace-agnostic captured-FAIL reconciliation (AC1 — closes the allowlist bypass).
|
|
1649
1654
|
// Fix A: status-independent — runs on EVERY stop. A claim contradicting the capture
|
|
1650
1655
|
// is a false-completion whether or not the agent says the task is 'done'.
|
|
1651
|
-
warnings.push(...capturedFailReconciliation(root,
|
|
1656
|
+
warnings.push(...capturedFailReconciliation(root, latestArtifactDir, taskStatus));
|
|
1652
1657
|
|
|
1653
1658
|
// Use module-scope HARD_BLOCK / FULL_BLOCK (defined above analyze()).
|
|
1654
1659
|
// pre-execution/terminal tasks: only HARD_BLOCK signals cause a block.
|
|
@@ -1686,7 +1691,8 @@ function resolveMaxBlocks() {
|
|
|
1686
1691
|
}
|
|
1687
1692
|
|
|
1688
1693
|
function blockStreakFile(root) {
|
|
1689
|
-
|
|
1694
|
+
const artifactRoot = flowAgentsArtifactRoot(root);
|
|
1695
|
+
return path.join(artifactRoot, '.goal-fit-block-streak.json');
|
|
1690
1696
|
}
|
|
1691
1697
|
|
|
1692
1698
|
function reasonsHash(warnings) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
|
|
20
|
+
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
20
21
|
|
|
21
22
|
const STEERING = {
|
|
22
23
|
'tool-planner': [
|
|
@@ -101,7 +102,8 @@ function readJson(file) {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
function latestWorkflowState(root) {
|
|
104
|
-
const states =
|
|
105
|
+
const states = flowAgentsArtifactRootsForRead(root)
|
|
106
|
+
.flatMap(artifactRoot => walkStateFiles(artifactRoot))
|
|
105
107
|
.map(file => {
|
|
106
108
|
let stat;
|
|
107
109
|
try { stat = fs.statSync(file); } catch { return null; }
|
|
@@ -295,8 +297,8 @@ function resumeSteering(root, current) {
|
|
|
295
297
|
|
|
296
298
|
// Liveness advisory
|
|
297
299
|
try {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
+
const events = flowAgentsArtifactRootsForRead(root)
|
|
301
|
+
.flatMap(artifactRoot => readLivenessEvents(path.join(artifactRoot, 'liveness', 'events.jsonl')));
|
|
300
302
|
if (events.length > 0) {
|
|
301
303
|
const selfActor = (process.env.FLOW_AGENTS_ACTOR || '').trim() || 'local';
|
|
302
304
|
const holders = freshHolders(events, slug, selfActor, Date.now());
|
|
@@ -51,6 +51,11 @@ function computeChainHash(prevHash, record) {
|
|
|
51
51
|
* - ANY `||` (e.g. `npm test || exit 0`, `|| echo ok`, `|| /bin/true`)
|
|
52
52
|
* - `| true` (pipe into true — the pipeline absorbs the exit code)
|
|
53
53
|
* - trailing `; true` / `; :` / `; exit 0` / `; /bin/true` (and `\n` variants)
|
|
54
|
+
*
|
|
55
|
+
* FROZEN bar-raiser (ADR 0018). Do NOT add new evasion-pattern rules here; route new
|
|
56
|
+
* laundering shapes to the external CI anchor (trust-reconcile + the anti-gaming suite).
|
|
57
|
+
* Accepted limitation: the blanket `||` rule over-blocks legitimate control-flow `||`
|
|
58
|
+
* (e.g. `test -d node_modules || npm ci`) — it fails toward blocking by design.
|
|
54
59
|
*/
|
|
55
60
|
function hasLaunderingOperator(cmd) {
|
|
56
61
|
// ANY || in a claimed verification command is an exit-code mask.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const VERITAS_GENERATED = new Set([
|
|
6
|
+
"evidence",
|
|
7
|
+
"claims",
|
|
8
|
+
"runs",
|
|
9
|
+
"repo-conformance",
|
|
10
|
+
"external",
|
|
11
|
+
"eval-drafts",
|
|
12
|
+
"evals",
|
|
13
|
+
"checkins",
|
|
14
|
+
"init-plans",
|
|
15
|
+
"recommendations",
|
|
16
|
+
"surface-console",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const opts = { repos: [], workspaces: [], apply: false, dryRun: true, json: false, includeFlowAgents: false, force: false };
|
|
21
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === "--apply") { opts.apply = true; opts.dryRun = false; continue; }
|
|
24
|
+
if (arg === "--dry-run") { opts.dryRun = true; opts.apply = false; continue; }
|
|
25
|
+
if (arg === "--json") { opts.json = true; continue; }
|
|
26
|
+
if (arg === "--include-flow-agents") { opts.includeFlowAgents = true; continue; }
|
|
27
|
+
if (arg === "--force") { opts.force = true; continue; }
|
|
28
|
+
if (arg === "--repo") { opts.repos.push(requireValue(argv, ++i, arg)); continue; }
|
|
29
|
+
if (arg === "--workspace") { opts.workspaces.push(requireValue(argv, ++i, arg)); continue; }
|
|
30
|
+
if (arg === "--help" || arg === "-h") { opts.help = true; continue; }
|
|
31
|
+
throw new Error(`unknown option: ${arg}`);
|
|
32
|
+
}
|
|
33
|
+
return opts;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireValue(argv, index, option) {
|
|
37
|
+
const value = argv[index];
|
|
38
|
+
if (!value || value.startsWith("--")) throw new Error(`${option} requires a path`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function usage() {
|
|
43
|
+
return `Usage: node scripts/migrate-local-artifacts.mjs [--workspace <path>] [--repo <path>] [--include-flow-agents] [--apply] [--force] [--json]
|
|
44
|
+
|
|
45
|
+
Dry-run is the default. --apply copies generated local artifacts into .kontourai without deleting old paths.
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unique(values) {
|
|
50
|
+
return [...new Set(values.map((value) => path.resolve(value)))];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function repoCandidates(opts) {
|
|
54
|
+
const repos = [...opts.repos.map((repo) => path.resolve(repo))];
|
|
55
|
+
const workspaces = opts.workspaces.length ? opts.workspaces : (opts.repos.length ? [] : [process.cwd()]);
|
|
56
|
+
for (const workspace of workspaces) {
|
|
57
|
+
const root = path.resolve(workspace);
|
|
58
|
+
if (isDirectory(root)) repos.push(root);
|
|
59
|
+
let entries = [];
|
|
60
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.isDirectory() || entry.name === "node_modules") continue;
|
|
63
|
+
repos.push(path.join(root, entry.name));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return unique(repos).filter(isDirectory);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isDirectory(file) {
|
|
70
|
+
try { return fs.statSync(file).isDirectory(); } catch { return false; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function exists(file) {
|
|
74
|
+
try { fs.lstatSync(file); return true; } catch { return false; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function symlinkStatus(file) {
|
|
78
|
+
try { return fs.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasSymlinkParent(repo, target) {
|
|
82
|
+
const relativeParent = path.relative(repo, path.dirname(target));
|
|
83
|
+
if (!relativeParent || relativeParent.startsWith("..") || path.isAbsolute(relativeParent)) return false;
|
|
84
|
+
let current = repo;
|
|
85
|
+
for (const part of relativeParent.split(path.sep)) {
|
|
86
|
+
if (!part) continue;
|
|
87
|
+
current = path.join(current, part);
|
|
88
|
+
if (symlinkStatus(current)) return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generatedMappings(repo, includeFlowAgents) {
|
|
94
|
+
const mappings = [
|
|
95
|
+
[".kontour", ".kontourai/console"],
|
|
96
|
+
[".telemetry", ".kontourai/telemetry"],
|
|
97
|
+
[".flow/runs", ".kontourai/flow/runs"],
|
|
98
|
+
[".surface/runs", ".kontourai/surface/runs"],
|
|
99
|
+
];
|
|
100
|
+
for (const name of VERITAS_GENERATED) mappings.push([`.veritas/${name}`, `.kontourai/veritas/${name}`]);
|
|
101
|
+
mappings.push([".veritas/standards-feedback", ".kontourai/veritas/standards-feedback"]);
|
|
102
|
+
mappings.push([".veritas/standards-feedback-drafts", ".kontourai/veritas/standards-feedback-drafts"]);
|
|
103
|
+
if (includeFlowAgents) mappings.push([".flow-agents", ".kontourai/flow-agents"]);
|
|
104
|
+
return mappings.map(([from, to]) => ({ repo, from: path.join(repo, from), to: path.join(repo, to), relFrom: from, relTo: to }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function durableSkips(repo, includeFlowAgents) {
|
|
108
|
+
const paths = [
|
|
109
|
+
".flow/config.json",
|
|
110
|
+
".flow/definitions",
|
|
111
|
+
".veritas/repo-map.json",
|
|
112
|
+
".veritas/repo-standards",
|
|
113
|
+
".veritas/authority",
|
|
114
|
+
".veritas/proof-families",
|
|
115
|
+
".agents",
|
|
116
|
+
".claude/commands",
|
|
117
|
+
"docs",
|
|
118
|
+
];
|
|
119
|
+
if (!includeFlowAgents) paths.push(".flow-agents");
|
|
120
|
+
return paths.map((rel) => ({ repo, path: path.join(repo, rel), rel, reason: "durable-or-not-included" })).filter((item) => exists(item.path));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function listFiles(root) {
|
|
124
|
+
const out = [];
|
|
125
|
+
const stack = [root];
|
|
126
|
+
while (stack.length) {
|
|
127
|
+
const dir = stack.pop();
|
|
128
|
+
let entries = [];
|
|
129
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const full = path.join(dir, entry.name);
|
|
132
|
+
if (entry.isSymbolicLink()) {
|
|
133
|
+
out.push({ file: full, symlink: true });
|
|
134
|
+
} else if (entry.isDirectory()) {
|
|
135
|
+
stack.push(full);
|
|
136
|
+
} else if (entry.isFile()) {
|
|
137
|
+
out.push({ file: full, symlink: false });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function planRepo(repo, opts) {
|
|
145
|
+
const copies = [];
|
|
146
|
+
const skipped = durableSkips(repo, opts.includeFlowAgents);
|
|
147
|
+
for (const mapping of generatedMappings(repo, opts.includeFlowAgents)) {
|
|
148
|
+
if (!exists(mapping.from)) continue;
|
|
149
|
+
const stat = fs.lstatSync(mapping.from);
|
|
150
|
+
if (stat.isSymbolicLink()) {
|
|
151
|
+
skipped.push({ repo, path: mapping.from, rel: mapping.relFrom, reason: "source-is-symlink" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (stat.isFile()) {
|
|
155
|
+
skipped.push({ repo, path: mapping.from, rel: mapping.relFrom, reason: "generated-root-is-file" });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!stat.isDirectory()) continue;
|
|
159
|
+
for (const item of listFiles(mapping.from)) {
|
|
160
|
+
const rel = path.relative(mapping.from, item.file);
|
|
161
|
+
const target = path.join(mapping.to, rel);
|
|
162
|
+
if (item.symlink) {
|
|
163
|
+
skipped.push({ repo, path: item.file, rel: path.join(mapping.relFrom, rel), reason: "source-is-symlink" });
|
|
164
|
+
} else if (hasSymlinkParent(repo, target)) {
|
|
165
|
+
skipped.push({ repo, path: item.file, rel: path.join(mapping.relFrom, rel), target, reason: "target-parent-is-symlink" });
|
|
166
|
+
} else if (exists(target)) {
|
|
167
|
+
if (symlinkStatus(target)) {
|
|
168
|
+
skipped.push({ repo, path: item.file, rel: path.join(mapping.relFrom, rel), target, reason: "target-is-symlink" });
|
|
169
|
+
} else if (!opts.force) {
|
|
170
|
+
skipped.push({ repo, path: item.file, rel: path.join(mapping.relFrom, rel), target, reason: "target-exists" });
|
|
171
|
+
} else {
|
|
172
|
+
copies.push({ repo, source: item.file, target, relSource: path.join(mapping.relFrom, rel), relTarget: path.join(mapping.relTo, rel) });
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
copies.push({ repo, source: item.file, target, relSource: path.join(mapping.relFrom, rel), relTarget: path.join(mapping.relTo, rel) });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { repo, copies, skipped };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function copyFile(source, target) {
|
|
183
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
184
|
+
fs.copyFileSync(source, target);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function run(argv = process.argv.slice(2)) {
|
|
188
|
+
const opts = parseArgs(argv);
|
|
189
|
+
if (opts.help) return { help: usage(), reports: [], summary: { repos: 0, copies: 0, skipped: 0, applied: false, dry_run: true } };
|
|
190
|
+
const reports = repoCandidates(opts).map((repo) => planRepo(repo, opts));
|
|
191
|
+
if (opts.apply) {
|
|
192
|
+
for (const report of reports) for (const copy of report.copies) copyFile(copy.source, copy.target);
|
|
193
|
+
}
|
|
194
|
+
const summary = {
|
|
195
|
+
repos: reports.length,
|
|
196
|
+
copies: reports.reduce((sum, report) => sum + report.copies.length, 0),
|
|
197
|
+
skipped: reports.reduce((sum, report) => sum + report.skipped.length, 0),
|
|
198
|
+
applied: opts.apply,
|
|
199
|
+
dry_run: !opts.apply,
|
|
200
|
+
};
|
|
201
|
+
return { reports, summary };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function printText(result) {
|
|
205
|
+
if (result.help) {
|
|
206
|
+
process.stdout.write(result.help);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(`Local artifact migration ${result.summary.dry_run ? "dry-run" : "apply"}`);
|
|
210
|
+
console.log(`Repositories scanned: ${result.summary.repos}`);
|
|
211
|
+
console.log(`Files to copy: ${result.summary.copies}`);
|
|
212
|
+
console.log(`Skipped paths: ${result.summary.skipped}`);
|
|
213
|
+
for (const report of result.reports) {
|
|
214
|
+
if (!report.copies.length && !report.skipped.length) continue;
|
|
215
|
+
console.log(`\n${report.repo}`);
|
|
216
|
+
for (const copy of report.copies) console.log(` COPY ${copy.relSource} -> ${copy.relTarget}`);
|
|
217
|
+
for (const skip of report.skipped) console.log(` SKIP ${skip.rel}${skip.target ? ` -> ${path.relative(report.repo, skip.target)}` : ""} (${skip.reason})`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
222
|
+
try {
|
|
223
|
+
const result = run();
|
|
224
|
+
if (parseArgs(process.argv.slice(2)).json) console.log(JSON.stringify(result, null, 2));
|
|
225
|
+
else printText(result);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(`migrate-local-artifacts: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -7,10 +7,9 @@ TELEMETRY_CONFIG_FILE="${TELEMETRY_CONFIG_FILE:-${TELEMETRY_DIR}/telemetry.conf}
|
|
|
7
7
|
# Defaults
|
|
8
8
|
TELEMETRY_ENABLED="${TELEMETRY_ENABLED:-true}"
|
|
9
9
|
# TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
|
|
10
|
-
# two levels up.
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.telemetry}"
|
|
10
|
+
# two levels up. Local runtime telemetry defaults under .kontourai/telemetry;
|
|
11
|
+
# explicit TELEMETRY_DATA_DIR still wins.
|
|
12
|
+
TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.kontourai/telemetry}"
|
|
14
13
|
TELEMETRY_SESSION_DIR="${TELEMETRY_SESSION_DIR:-${TELEMETRY_DATA_DIR}/sessions}"
|
|
15
14
|
TELEMETRY_ENRICH_SYSTEM="${TELEMETRY_ENRICH_SYSTEM:-true}"
|
|
16
15
|
TELEMETRY_ENRICH_WORKSPACE="${TELEMETRY_ENRICH_WORKSPACE:-true}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
defaultArtifactRootForRead,
|
|
7
|
+
flowAgentsArtifactRoot,
|
|
8
|
+
KONTOURAI_DIR,
|
|
9
|
+
legacyFlowAgentsArtifactRoot,
|
|
10
|
+
LEGACY_FLOW_AGENTS_DIR,
|
|
11
|
+
} from "../../build/src/index.js";
|
|
12
|
+
|
|
13
|
+
test("public API exports local artifact root helpers", () => {
|
|
14
|
+
const cwd = path.resolve("/tmp/flow-agents-public-api");
|
|
15
|
+
|
|
16
|
+
assert.equal(KONTOURAI_DIR, ".kontourai");
|
|
17
|
+
assert.equal(LEGACY_FLOW_AGENTS_DIR, ".flow-agents");
|
|
18
|
+
assert.equal(flowAgentsArtifactRoot(cwd), path.join(cwd, ".kontourai", "flow-agents"));
|
|
19
|
+
assert.equal(legacyFlowAgentsArtifactRoot(cwd), path.join(cwd, ".flow-agents"));
|
|
20
|
+
assert.equal(defaultArtifactRootForRead(cwd), path.join(cwd, ".kontourai", "flow-agents"));
|
|
21
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Claim explanation — a PURE projection extracted from workflow-sidecar.ts (ops#22).
|
|
2
|
+
//
|
|
3
|
+
// buildClaimExplanation is a pure function: report + bundle + id in, structured
|
|
4
|
+
// explanation out. No fs, no CLI, no .flow-agents paths, no shared module state —
|
|
5
|
+
// zero flow-agents specifics inside it, so it is unit-testable in isolation and can
|
|
6
|
+
// be lifted to Surface unchanged (issue #171). The IO command handler `claimLookup`
|
|
7
|
+
// that consumes it stays in workflow-sidecar.ts.
|
|
8
|
+
|
|
9
|
+
type AnyObj = Record<string, any>;
|
|
10
|
+
|
|
11
|
+
export interface ClaimEvidenceItem {
|
|
12
|
+
evidenceType: string;
|
|
13
|
+
label: string;
|
|
14
|
+
execution: { runner: string; label: string; isError: boolean; exitCode: number | null } | null;
|
|
15
|
+
passing: boolean;
|
|
16
|
+
summary: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ClaimExplanation {
|
|
20
|
+
found: boolean;
|
|
21
|
+
status: string;
|
|
22
|
+
value: string;
|
|
23
|
+
claimType: string;
|
|
24
|
+
evidence: ClaimEvidenceItem[];
|
|
25
|
+
policy: {
|
|
26
|
+
id: string;
|
|
27
|
+
requiredEvidence: string[];
|
|
28
|
+
requiredMethods?: string[];
|
|
29
|
+
acceptanceCriteria: string[];
|
|
30
|
+
reviewAuthority: string;
|
|
31
|
+
} | null;
|
|
32
|
+
why: {
|
|
33
|
+
directInputs: AnyObj[];
|
|
34
|
+
leafClaims: AnyObj[];
|
|
35
|
+
diagnostics: AnyObj[];
|
|
36
|
+
transparencyGaps: AnyObj[];
|
|
37
|
+
changeRecords: AnyObj[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build a structured explanation for a specific claim.
|
|
43
|
+
* PURE: report + bundle + id in, structured explanation out.
|
|
44
|
+
* No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
|
|
45
|
+
*
|
|
46
|
+
* @param report TrustReport from buildTrustReport(bundle) — required for derived status
|
|
47
|
+
* @param bundle Raw parsed trust.bundle (BundleFile shape)
|
|
48
|
+
* @param claimId The claim id to explain
|
|
49
|
+
*/
|
|
50
|
+
export function buildClaimExplanation(
|
|
51
|
+
report: Record<string, unknown>,
|
|
52
|
+
bundle: Record<string, unknown>,
|
|
53
|
+
claimId: string,
|
|
54
|
+
): ClaimExplanation {
|
|
55
|
+
const reportClaims = Array.isArray(report.claims) ? (report.claims as AnyObj[]) : [];
|
|
56
|
+
const reportClaim = reportClaims.find((c: AnyObj) => c.id === claimId);
|
|
57
|
+
|
|
58
|
+
if (!reportClaim) {
|
|
59
|
+
return {
|
|
60
|
+
found: false,
|
|
61
|
+
status: "unknown",
|
|
62
|
+
value: "",
|
|
63
|
+
claimType: "",
|
|
64
|
+
evidence: [],
|
|
65
|
+
policy: null,
|
|
66
|
+
why: { directInputs: [], leafClaims: [], diagnostics: [], transparencyGaps: [], changeRecords: [] },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bundleClaims = Array.isArray(bundle.claims) ? (bundle.claims as AnyObj[]) : [];
|
|
71
|
+
const bundleClaim = bundleClaims.find((c: AnyObj) => c.id === claimId) ?? reportClaim;
|
|
72
|
+
const bundlePolicies = Array.isArray(bundle.policies) ? (bundle.policies as AnyObj[]) : [];
|
|
73
|
+
const bundleEvidence = Array.isArray(bundle.evidence) ? (bundle.evidence as AnyObj[]) : [];
|
|
74
|
+
|
|
75
|
+
// Governing policy — follow verificationPolicyId into bundle.policies[]
|
|
76
|
+
const verificationPolicyId = typeof bundleClaim.verificationPolicyId === "string" ? bundleClaim.verificationPolicyId : undefined;
|
|
77
|
+
const rawPolicy = verificationPolicyId ? bundlePolicies.find((p: AnyObj) => p.id === verificationPolicyId) : undefined;
|
|
78
|
+
const policy = rawPolicy
|
|
79
|
+
? {
|
|
80
|
+
id: String(rawPolicy.id ?? ""),
|
|
81
|
+
requiredEvidence: Array.isArray(rawPolicy.requiredEvidence) ? (rawPolicy.requiredEvidence as string[]) : [],
|
|
82
|
+
requiredMethods: Array.isArray(rawPolicy.requiredMethods) ? (rawPolicy.requiredMethods as string[]) : undefined,
|
|
83
|
+
acceptanceCriteria: Array.isArray(rawPolicy.acceptanceCriteria) ? (rawPolicy.acceptanceCriteria as string[]) : [],
|
|
84
|
+
reviewAuthority: String(rawPolicy.reviewAuthority ?? ""),
|
|
85
|
+
}
|
|
86
|
+
: null;
|
|
87
|
+
|
|
88
|
+
// Evidence enhancement: pull evidence items for this claim, surface the execution block
|
|
89
|
+
const claimEvidenceItems = bundleEvidence.filter((ev: AnyObj) => ev && ev.claimId === claimId);
|
|
90
|
+
const evidence: ClaimEvidenceItem[] = claimEvidenceItems.map((ev: AnyObj) => {
|
|
91
|
+
const exec = ev.execution && typeof ev.execution === "object" ? (ev.execution as AnyObj) : null;
|
|
92
|
+
const execution = exec
|
|
93
|
+
? {
|
|
94
|
+
runner: String(exec.runner ?? exec.label ?? ""),
|
|
95
|
+
label: String(exec.label ?? exec.runner ?? ""),
|
|
96
|
+
isError: Boolean(exec.isError ?? (typeof exec.exitCode === "number" && exec.exitCode !== 0)),
|
|
97
|
+
exitCode: typeof exec.exitCode === "number" ? exec.exitCode : null,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
return {
|
|
101
|
+
evidenceType: String(ev.evidenceType ?? ev.type ?? "unknown"),
|
|
102
|
+
label: String(ev.label ?? ev.excerptOrSummary ?? ev.sourceRef ?? ev.id ?? ""),
|
|
103
|
+
execution,
|
|
104
|
+
passing: execution ? !execution.isError : String(ev.status ?? "") !== "disputed",
|
|
105
|
+
summary: String(ev.excerptOrSummary ?? ev.summary ?? ev.label ?? ""),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Drilldown: extract from report structure (report.transparencyGaps, report.changeRecords)
|
|
110
|
+
const allGaps = Array.isArray(report.transparencyGaps) ? (report.transparencyGaps as AnyObj[]) : [];
|
|
111
|
+
const allChanges = Array.isArray(report.changeRecords) ? (report.changeRecords as AnyObj[]) : [];
|
|
112
|
+
const transparencyGaps = allGaps.filter((g: AnyObj) => g && g.claimId === claimId);
|
|
113
|
+
const changeRecords = allChanges.filter((c: AnyObj) => c && c.claimId === claimId);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
found: true,
|
|
117
|
+
status: String(reportClaim.status ?? "unknown"),
|
|
118
|
+
value: String(bundleClaim.value ?? reportClaim.value ?? ""),
|
|
119
|
+
claimType: String(bundleClaim.claimType ?? reportClaim.claimType ?? ""),
|
|
120
|
+
evidence,
|
|
121
|
+
policy,
|
|
122
|
+
why: {
|
|
123
|
+
directInputs: [], // populated by buildDerivationDrilldown if non-leaf
|
|
124
|
+
leafClaims: [],
|
|
125
|
+
diagnostics: [],
|
|
126
|
+
transparencyGaps,
|
|
127
|
+
changeRecords,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|