@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,168 @@
|
|
|
1
|
+
// Thin TypeScript-layer unit tests for the PURE helpers in workflow-sidecar (ops#22).
|
|
2
|
+
//
|
|
3
|
+
// Until now the ~1,924 assertions covering this module were all black-box bash that
|
|
4
|
+
// drove the CLI. These tests exercise the pure, side-effect-free helpers directly
|
|
5
|
+
// against the built JS — fast, deterministic, and isolating logic from fs/CLI. They
|
|
6
|
+
// complement (do not replace) the bash evals.
|
|
7
|
+
//
|
|
8
|
+
// Run: `npm run test:unit` (builds first). Requires `npm run build` output under build/.
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
buildClaimExplanation as buildClaimExplanationReexport,
|
|
14
|
+
deriveGateCalibration,
|
|
15
|
+
gateAdvisoryFix,
|
|
16
|
+
buildGateInquiryRecords,
|
|
17
|
+
validateEvidenceRef,
|
|
18
|
+
normalizeEvidenceRefs,
|
|
19
|
+
normalizeCheck,
|
|
20
|
+
} from "../../build/src/cli/workflow-sidecar.js";
|
|
21
|
+
import { buildClaimExplanation } from "../../build/src/cli/sidecar-claim-explain.js";
|
|
22
|
+
|
|
23
|
+
// ── buildClaimExplanation (extracted pure projection) ────────────────────────
|
|
24
|
+
|
|
25
|
+
test("buildClaimExplanation: re-export from workflow-sidecar === the extracted module", () => {
|
|
26
|
+
assert.equal(buildClaimExplanationReexport, buildClaimExplanation);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("buildClaimExplanation: unknown claim id returns found:false sentinel", () => {
|
|
30
|
+
const out = buildClaimExplanation({ claims: [] }, { claims: [] }, "missing");
|
|
31
|
+
assert.equal(out.found, false);
|
|
32
|
+
assert.equal(out.status, "unknown");
|
|
33
|
+
assert.deepEqual(out.evidence, []);
|
|
34
|
+
assert.equal(out.policy, null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("buildClaimExplanation: projects status, policy, and evidence for a found claim", () => {
|
|
38
|
+
const report = {
|
|
39
|
+
claims: [{ id: "c1", status: "verified", value: "pass", claimType: "workflow.check" }],
|
|
40
|
+
transparencyGaps: [{ claimId: "c1", reason: "x" }, { claimId: "other" }],
|
|
41
|
+
changeRecords: [],
|
|
42
|
+
};
|
|
43
|
+
const bundle = {
|
|
44
|
+
claims: [{ id: "c1", verificationPolicyId: "p1", value: "pass", claimType: "workflow.check" }],
|
|
45
|
+
policies: [{ id: "p1", requiredEvidence: ["test"], acceptanceCriteria: ["AC1"], reviewAuthority: "owner" }],
|
|
46
|
+
evidence: [{ claimId: "c1", evidenceType: "command", execution: { runner: "npm test", label: "npm test", exitCode: 0 } }],
|
|
47
|
+
};
|
|
48
|
+
const out = buildClaimExplanation(report, bundle, "c1");
|
|
49
|
+
assert.equal(out.found, true);
|
|
50
|
+
assert.equal(out.status, "verified");
|
|
51
|
+
assert.equal(out.policy.id, "p1");
|
|
52
|
+
assert.deepEqual(out.policy.requiredEvidence, ["test"]);
|
|
53
|
+
assert.equal(out.evidence.length, 1);
|
|
54
|
+
assert.equal(out.evidence[0].passing, true); // exitCode 0 → not error → passing
|
|
55
|
+
assert.equal(out.evidence[0].execution.exitCode, 0);
|
|
56
|
+
assert.equal(out.why.transparencyGaps.length, 1); // only the c1 gap, not "other"
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── deriveGateCalibration (pure mapping) ─────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
test("deriveGateCalibration: maps outcome/status/blocked to a calibration verdict", () => {
|
|
62
|
+
assert.equal(deriveGateCalibration("unsupported", undefined, true), "missed_block");
|
|
63
|
+
assert.equal(deriveGateCalibration("matched", "disputed", true), "correct");
|
|
64
|
+
assert.equal(deriveGateCalibration("matched", "rejected", true), "correct");
|
|
65
|
+
assert.equal(deriveGateCalibration("matched", "verified", true), "false_block");
|
|
66
|
+
assert.equal(deriveGateCalibration("matched", "stale", true), "false_block"); // blocked w/o solid evidence
|
|
67
|
+
assert.equal(deriveGateCalibration("matched", "stale", false), "missed_block");
|
|
68
|
+
assert.equal(deriveGateCalibration("matched", "verified", false), "correct");
|
|
69
|
+
assert.equal(deriveGateCalibration("derived", "proposed", false), "missed_block");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── gateAdvisoryFix (pure string composition) ────────────────────────────────
|
|
73
|
+
|
|
74
|
+
test("gateAdvisoryFix: composes calibration-specific guidance naming the claim", () => {
|
|
75
|
+
assert.match(gateAdvisoryFix("correct", "c1", "disputed"), /No gate change needed/);
|
|
76
|
+
assert.match(gateAdvisoryFix("correct", "c1", "disputed"), /`c1`/);
|
|
77
|
+
assert.match(gateAdvisoryFix("false_block", "c1", "verified"), /Investigate why the gate blocked/);
|
|
78
|
+
assert.match(gateAdvisoryFix("missed_block", "c1", "stale"), /Refresh the stale claim/);
|
|
79
|
+
assert.match(gateAdvisoryFix("missed_block", "c1", "absent"), /writes a bundle claim/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── validateEvidenceRef / normalizeEvidenceRefs (pure validators) ────────────
|
|
83
|
+
|
|
84
|
+
test("validateEvidenceRef: accepts a well-formed source ref", () => {
|
|
85
|
+
const ref = { kind: "source", file: "a.ts", line_start: 1, line_end: 2, excerpt: "x" };
|
|
86
|
+
assert.deepEqual(validateEvidenceRef({ ...ref }, "refs"), ref);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("validateEvidenceRef: rejects bad kind, unsupported field, and incomplete source ref", () => {
|
|
90
|
+
assert.throws(() => validateEvidenceRef({ kind: "nope" }, "refs"), /kind must be one of/);
|
|
91
|
+
assert.throws(() => validateEvidenceRef({ kind: "command", bogus: 1, summary: "s" }, "refs"), /unsupported field/);
|
|
92
|
+
assert.throws(() => validateEvidenceRef({ kind: "source", file: "a.ts" }, "refs"), /source refs require/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("normalizeEvidenceRefs: rejects non-arrays and legacy string refs; passes valid arrays", () => {
|
|
96
|
+
assert.throws(() => normalizeEvidenceRefs("nope", "refs"), /must be an array/);
|
|
97
|
+
assert.throws(() => normalizeEvidenceRefs(["legacy"], "refs"), /legacy string refs are not supported/);
|
|
98
|
+
const ok = normalizeEvidenceRefs([{ kind: "command", summary: "ran tests" }], "refs");
|
|
99
|
+
assert.equal(ok.length, 1);
|
|
100
|
+
assert.equal(ok[0].kind, "command");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── normalizeCheck (pure path: no surface_trust_refs) ────────────────────────
|
|
104
|
+
|
|
105
|
+
test("normalizeCheck: validates required fields, kind, and status", () => {
|
|
106
|
+
assert.throws(() => normalizeCheck({ id: "x" }), /requires id, kind, status/);
|
|
107
|
+
assert.throws(() => normalizeCheck({ id: "x", kind: "bogus", status: "pass", summary: "s" }), /kind must be one of/);
|
|
108
|
+
assert.throws(() => normalizeCheck({ id: "x", kind: "test", status: "bogus", summary: "s" }), /status must be one of/);
|
|
109
|
+
const ok = normalizeCheck({ id: "x", kind: "test", status: "pass", summary: "ran" });
|
|
110
|
+
assert.equal(ok.kind, "test");
|
|
111
|
+
assert.equal(ok.status, "pass");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── buildGateInquiryRecords (pure orchestration, fake Surface injected) ───────
|
|
115
|
+
|
|
116
|
+
test("buildGateInquiryRecords: resolves each claim through the injected Surface and tags calibration", () => {
|
|
117
|
+
const fakeSurface = {
|
|
118
|
+
resolveInquiry: (_bundle, inquiry, _opts) => ({
|
|
119
|
+
id: inquiry.id,
|
|
120
|
+
inquiry,
|
|
121
|
+
outcome: "matched",
|
|
122
|
+
resolutionPath: { claimIds: [inquiry.metadata.claimId] },
|
|
123
|
+
inputSnapshot: {},
|
|
124
|
+
statusFunctionVersion: "test",
|
|
125
|
+
resolvedAt: "2026-01-01T00:00:00Z",
|
|
126
|
+
answer: { status: "disputed" },
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
const bundle = {
|
|
130
|
+
schemaVersion: 2,
|
|
131
|
+
source: "s",
|
|
132
|
+
claims: [{ id: "c1", subjectType: "workflow-check", subjectId: "slug/AC1", fieldOrBehavior: "AC1", status: "disputed" }],
|
|
133
|
+
evidence: [], events: [], policies: [],
|
|
134
|
+
};
|
|
135
|
+
const records = buildGateInquiryRecords(
|
|
136
|
+
bundle,
|
|
137
|
+
{ blocked: true, hash: "h", count: 1 },
|
|
138
|
+
"slug",
|
|
139
|
+
[],
|
|
140
|
+
fakeSurface,
|
|
141
|
+
new Date("2026-01-01T00:00:00Z"),
|
|
142
|
+
);
|
|
143
|
+
assert.equal(records.length, 1);
|
|
144
|
+
assert.equal(records[0].outcome, "matched");
|
|
145
|
+
// blocked + disputed → "correct" calibration in answer.value
|
|
146
|
+
assert.equal(records[0].answer.value.calibration, "correct");
|
|
147
|
+
assert.equal(records[0].answer.value.gateFired, true);
|
|
148
|
+
assert.equal(records[0].answer.value.sessionSlug, "slug");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("buildGateInquiryRecords: emits a single missed_block record for an empty bundle", () => {
|
|
152
|
+
const fakeSurface = {
|
|
153
|
+
resolveInquiry: (_bundle, inquiry, _opts) => ({
|
|
154
|
+
id: inquiry.id,
|
|
155
|
+
inquiry,
|
|
156
|
+
outcome: "unsupported",
|
|
157
|
+
resolutionPath: { claimIds: [] },
|
|
158
|
+
inputSnapshot: {},
|
|
159
|
+
statusFunctionVersion: "test",
|
|
160
|
+
resolvedAt: "2026-01-01T00:00:00Z",
|
|
161
|
+
answer: { status: "unknown" },
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
const bundle = { schemaVersion: 2, source: "s", claims: [], evidence: [], events: [], policies: [] };
|
|
165
|
+
const records = buildGateInquiryRecords(bundle, { blocked: false, hash: null, count: 0 }, "slug", [], fakeSurface, new Date("2026-01-01T00:00:00Z"));
|
|
166
|
+
assert.equal(records.length, 1);
|
|
167
|
+
assert.equal(records[0].answer.value.calibration, "missed_block");
|
|
168
|
+
});
|
|
@@ -3,6 +3,7 @@ import * as http from "node:http";
|
|
|
3
3
|
import * as https from "node:https";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
6
|
+
import { telemetryDataDir as defaultTelemetryDataDir } from "../lib/local-artifact-root.js";
|
|
6
7
|
|
|
7
8
|
type Config = Record<string, string>;
|
|
8
9
|
|
|
@@ -82,10 +83,7 @@ function channelConfigValue(config: Config, channel: string, key: string, fallba
|
|
|
82
83
|
|
|
83
84
|
function telemetryDataDir(dest: string): string {
|
|
84
85
|
const configured = process.env.TELEMETRY_DATA_DIR;
|
|
85
|
-
|
|
86
|
-
// workspace at <dest>/.telemetry. The previous "../.telemetry" duplicated
|
|
87
|
-
// the parent-escape bug fixed in config.sh on 2026-06-11.
|
|
88
|
-
return configured ? path.resolve(dest, configured) : path.resolve(dest, ".telemetry");
|
|
86
|
+
return configured ? path.resolve(dest, configured) : defaultTelemetryDataDir(dest);
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
function deriveConsoleEndpoint(consoleUrl: string, explicitEndpoint: string): string {
|
|
@@ -3,11 +3,13 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
|
|
6
|
+
import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot, legacyFlowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
6
7
|
|
|
7
8
|
const VALID_RESULTS = new Set(["success", "partial", "failure", "not_verified"]);
|
|
8
9
|
|
|
9
10
|
function telemetryDir(flags: Record<string, string | boolean | string[]>): string {
|
|
10
|
-
|
|
11
|
+
const explicit = flagString(flags, "telemetry-dir") ?? process.env.TELEMETRY_DATA_DIR;
|
|
12
|
+
return explicit ? path.resolve(explicit) : telemetryDataDir();
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
function ensureSafeDir(dir: string): void {
|
|
@@ -110,7 +112,8 @@ function normalize(input: Record<string, unknown>[], runtime: string, flags: Rec
|
|
|
110
112
|
function importTelemetry(argv: string[], defaultRuntime?: string): number {
|
|
111
113
|
const { flags } = parseArgs(argv);
|
|
112
114
|
const runtime = defaultRuntime ?? flagString(flags, "runtime", "codex") ?? "codex";
|
|
113
|
-
const
|
|
115
|
+
const explicitInputDir = flagString(flags, "input-telemetry-dir");
|
|
116
|
+
const input = flagString(flags, "input-full-jsonl") ?? path.join(explicitInputDir ? path.resolve(explicitInputDir) : defaultTelemetryDirForRead(), "full.jsonl");
|
|
114
117
|
if (!input || !fs.existsSync(input)) throw new Error(`input telemetry file does not exist: ${input}`);
|
|
115
118
|
const dir = telemetryDir(flags);
|
|
116
119
|
ensureSafeDir(dir);
|
|
@@ -143,7 +146,7 @@ function syncArtifacts(argv: string[]): number {
|
|
|
143
146
|
const dir = telemetryDir(flags);
|
|
144
147
|
ensureSafeDir(dir);
|
|
145
148
|
const artifacts = flagList(flags, "artifact-dir");
|
|
146
|
-
const records = (artifacts.length ? artifacts : [
|
|
149
|
+
const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot(), legacyFlowAgentsArtifactRoot()]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
|
|
147
150
|
writeJsonlUpsert(path.join(dir, "outcomes.jsonl"), records, "outcome_id");
|
|
148
151
|
if (!flagBool(flags, "quiet")) console.log(`synced ${records.length} artifact outcome(s) to ${path.join(dir, "outcomes.jsonl")}`);
|
|
149
152
|
return 0;
|
|
@@ -288,7 +291,9 @@ function markdownReport(data: Record<string, unknown>, groupBy?: string): string
|
|
|
288
291
|
|
|
289
292
|
function report(argv: string[]): number {
|
|
290
293
|
const { flags } = parseArgs(argv);
|
|
291
|
-
const
|
|
294
|
+
const requestedDirs = flagList(flags, "telemetry-dir");
|
|
295
|
+
const dirs = (requestedDirs.length ? requestedDirs.map((dir) => path.resolve(dir)) : defaultTelemetryDirsForRead()).filter(Boolean);
|
|
296
|
+
if (dirs.length === 0) dirs.push(telemetryDataDir());
|
|
292
297
|
dirs.forEach(ensureSafeDir);
|
|
293
298
|
const data = reportData(dirs, flagString(flags, "group-by"));
|
|
294
299
|
const format = flagString(flags, "format", "markdown");
|
|
@@ -319,7 +324,7 @@ function registerProject(argv: string[]): number {
|
|
|
319
324
|
ensureSafeDir(globalDir);
|
|
320
325
|
const repoRoot = path.resolve(flagString(flags, "repo-root", ".") ?? ".");
|
|
321
326
|
const name = flagString(flags, "name", path.basename(repoRoot)) ?? path.basename(repoRoot);
|
|
322
|
-
const record = { name, repo_root: repoRoot, artifact_dir:
|
|
327
|
+
const record = { name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: flagString(flags, "runtime", "codex"), repo: flagString(flags, "repo", name), agent: flagString(flags, "agent"), profile_id: flagString(flags, "profile-id"), prompt_id: flagString(flags, "prompt-id"), prompt_variant: flagString(flags, "prompt-variant"), skill_ids: flagList(flags, "skill-id"), skill_variant: flagString(flags, "skill-variant") };
|
|
323
328
|
const registryFile = path.join(globalDir, "projects.json");
|
|
324
329
|
const existing = fs.existsSync(registryFile) ? JSON.parse(fs.readFileSync(registryFile, "utf8")) : { projects: [] };
|
|
325
330
|
const projects = Array.isArray(existing) ? existing : Array.isArray(existing.projects) ? existing.projects : [];
|
|
@@ -342,7 +347,9 @@ function syncProject(project: Record<string, unknown>, globalDir: string): void
|
|
|
342
347
|
const name = String(project.name ?? project.repo ?? "project").replace(/[^a-zA-Z0-9_.-]+/g, "-") || "project";
|
|
343
348
|
const store = path.join(globalDir, "projects", name);
|
|
344
349
|
ensureSafeDir(store);
|
|
345
|
-
const
|
|
350
|
+
const repoRoot = String(project.repo_root ?? process.cwd());
|
|
351
|
+
const configuredArtifactDir = typeof project.artifact_dir === "string" ? project.artifact_dir : "";
|
|
352
|
+
const artifactDir = configuredArtifactDir && fs.existsSync(configuredArtifactDir) ? configuredArtifactDir : defaultArtifactRootForRead(repoRoot);
|
|
346
353
|
const flags: Record<string, string | boolean | string[]> = {
|
|
347
354
|
"repo": String(project.repo ?? name),
|
|
348
355
|
"runtime": String(project.runtime ?? "codex"),
|
|
@@ -361,9 +368,9 @@ function syncProject(project: Record<string, unknown>, globalDir: string): void
|
|
|
361
368
|
function discoverProjects(root: string): Record<string, unknown>[] {
|
|
362
369
|
if (!fs.existsSync(root)) return [];
|
|
363
370
|
const candidates = [root, ...fs.readdirSync(root).map((name) => path.join(root, name))];
|
|
364
|
-
return candidates.filter((candidate) => fs.existsSync(
|
|
371
|
+
return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate)) || fs.existsSync(legacyFlowAgentsArtifactRoot(candidate))).map((repoRoot) => {
|
|
365
372
|
const name = path.basename(repoRoot);
|
|
366
|
-
return { name, repo: name, repo_root: repoRoot, artifact_dir:
|
|
373
|
+
return { name, repo: name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: "codex", skill_ids: [] };
|
|
367
374
|
});
|
|
368
375
|
}
|
|
369
376
|
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { flagBool, flagString, parseArgs } from "../lib/args.js";
|
|
5
|
+
import { defaultArtifactRootForRead } from "../lib/local-artifact-root.js";
|
|
5
6
|
|
|
6
7
|
type Classification =
|
|
7
8
|
| "active_wip"
|
|
@@ -49,7 +50,7 @@ function printHelp(): void {
|
|
|
49
50
|
console.log("Read-only dry-run audit for local workflow artifact directories.");
|
|
50
51
|
console.log("");
|
|
51
52
|
console.log("Options:");
|
|
52
|
-
console.log(" --artifact-root <path> Local artifact root to scan (default: .flow-agents)");
|
|
53
|
+
console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents, with .flow-agents read fallback)");
|
|
53
54
|
console.log(" --json Print stable JSON buckets instead of text");
|
|
54
55
|
console.log(" --help Show this help");
|
|
55
56
|
console.log("");
|
|
@@ -269,7 +270,8 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
269
270
|
}
|
|
270
271
|
let result: AuditResult;
|
|
271
272
|
try {
|
|
272
|
-
|
|
273
|
+
const root = flagString(args.flags, "artifact-root") ? path.resolve(flagString(args.flags, "artifact-root")!) : defaultArtifactRootForRead();
|
|
274
|
+
result = audit(root);
|
|
273
275
|
} catch (error) {
|
|
274
276
|
console.error(`workflow-artifact-cleanup-audit: ${error instanceof Error ? error.message : String(error)}`);
|
|
275
277
|
return 1;
|
|
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
9
9
|
import { resolveActiveFlowStep, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy, type ActiveFlowStep } from "../lib/flow-resolver.js";
|
|
10
|
+
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
10
11
|
|
|
11
12
|
type AnyObj = Record<string, any>;
|
|
12
13
|
|
|
@@ -652,7 +653,7 @@ function isUnderDir(dir: string, root: string): boolean {
|
|
|
652
653
|
function explicitArtifactRoot(p: ReturnType<typeof parseArgs>): string {
|
|
653
654
|
const explicit = opt(p, "artifact-dir");
|
|
654
655
|
const configuredRoot = opt(p, "artifact-root");
|
|
655
|
-
if (!explicit) return path.resolve(configuredRoot
|
|
656
|
+
if (!explicit) return configuredRoot ? path.resolve(configuredRoot) : flowAgentsArtifactRoot();
|
|
656
657
|
const dir = path.resolve(explicit);
|
|
657
658
|
if (!fs.existsSync(dir)) die(`artifact directory does not exist: ${dir}`);
|
|
658
659
|
if (fs.lstatSync(dir).isSymbolicLink()) die(`artifact directory must not be a symlink: ${dir}`);
|
|
@@ -879,7 +880,7 @@ function initSidecars(dir: string, slug: string, sourceRequest: string, summary:
|
|
|
879
880
|
}
|
|
880
881
|
|
|
881
882
|
function ensureSession(p: ReturnType<typeof parseArgs>): number {
|
|
882
|
-
const root =
|
|
883
|
+
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
883
884
|
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)"));
|
|
884
885
|
const dir = sessionDirFor(root, slug);
|
|
885
886
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -912,7 +913,7 @@ function ensureSession(p: ReturnType<typeof parseArgs>): number {
|
|
|
912
913
|
}
|
|
913
914
|
|
|
914
915
|
function current(p: ReturnType<typeof parseArgs>): number {
|
|
915
|
-
const root =
|
|
916
|
+
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
|
|
916
917
|
const dir = currentDir(root);
|
|
917
918
|
if (!dir) die("no current workflow session is recorded");
|
|
918
919
|
const format = opt(p, "format", "path");
|
|
@@ -1829,7 +1830,7 @@ function assertExistingLearningValid(dir: string): void {
|
|
|
1829
1830
|
}
|
|
1830
1831
|
}
|
|
1831
1832
|
async function dogfoodPass(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
1832
|
-
const root =
|
|
1833
|
+
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
|
|
1833
1834
|
const dir = path.resolve(opt(p, "artifact-dir") || currentDir(root) || "");
|
|
1834
1835
|
requireArtifactDirUnderRoot(dir, root);
|
|
1835
1836
|
assertExistingLearningValid(dir);
|
|
@@ -2265,7 +2266,7 @@ function loadSurfacePanelJs(): string {
|
|
|
2265
2266
|
}
|
|
2266
2267
|
|
|
2267
2268
|
async function renderTrustPanel(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
2268
|
-
const root =
|
|
2269
|
+
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
|
|
2269
2270
|
const dir = p.positional[0] ? artifactDirFrom(p.positional[0]) : currentDir(root);
|
|
2270
2271
|
if (!dir) die("render-trust-panel requires a workflow dir or a recorded current session");
|
|
2271
2272
|
let bundle: AnyObj | null = null;
|
|
@@ -2431,7 +2432,7 @@ function livenessLifecycle(taskDir: string, slug: string, kind: "claim" | "heart
|
|
|
2431
2432
|
}
|
|
2432
2433
|
|
|
2433
2434
|
async function liveness(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
2434
|
-
const root =
|
|
2435
|
+
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2435
2436
|
const action = p.positional[0] || "";
|
|
2436
2437
|
const subjectId = p.positional[1] || "";
|
|
2437
2438
|
const actor = opt(p, "actor", process.env.FLOW_AGENTS_ACTOR || "unknown");
|
|
@@ -2481,130 +2482,13 @@ async function liveness(p: ReturnType<typeof parseArgs>): Promise<number> {
|
|
|
2481
2482
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2482
2483
|
|
|
2483
2484
|
// ─── Claim Lookup — pure helper (promotable to Surface #171) ─────────────────
|
|
2484
|
-
// buildClaimExplanation
|
|
2485
|
-
//
|
|
2486
|
-
//
|
|
2487
|
-
|
|
2488
|
-
export
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
execution: { runner: string; label: string; isError: boolean; exitCode: number | null } | null;
|
|
2492
|
-
passing: boolean;
|
|
2493
|
-
summary: string;
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
export interface ClaimExplanation {
|
|
2497
|
-
found: boolean;
|
|
2498
|
-
status: string;
|
|
2499
|
-
value: string;
|
|
2500
|
-
claimType: string;
|
|
2501
|
-
evidence: ClaimEvidenceItem[];
|
|
2502
|
-
policy: {
|
|
2503
|
-
id: string;
|
|
2504
|
-
requiredEvidence: string[];
|
|
2505
|
-
requiredMethods?: string[];
|
|
2506
|
-
acceptanceCriteria: string[];
|
|
2507
|
-
reviewAuthority: string;
|
|
2508
|
-
} | null;
|
|
2509
|
-
why: {
|
|
2510
|
-
directInputs: AnyObj[];
|
|
2511
|
-
leafClaims: AnyObj[];
|
|
2512
|
-
diagnostics: AnyObj[];
|
|
2513
|
-
transparencyGaps: AnyObj[];
|
|
2514
|
-
changeRecords: AnyObj[];
|
|
2515
|
-
};
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
/**
|
|
2519
|
-
* Build a structured explanation for a specific claim.
|
|
2520
|
-
* PURE: report + bundle + id in, structured explanation out.
|
|
2521
|
-
* No fs, no CLI, no .flow-agents paths. Promotable to Surface #171.
|
|
2522
|
-
*
|
|
2523
|
-
* @param report TrustReport from buildTrustReport(bundle) — required for derived status
|
|
2524
|
-
* @param bundle Raw parsed trust.bundle (BundleFile shape)
|
|
2525
|
-
* @param claimId The claim id to explain
|
|
2526
|
-
*/
|
|
2527
|
-
export function buildClaimExplanation(
|
|
2528
|
-
report: Record<string, unknown>,
|
|
2529
|
-
bundle: Record<string, unknown>,
|
|
2530
|
-
claimId: string,
|
|
2531
|
-
): ClaimExplanation {
|
|
2532
|
-
const reportClaims = Array.isArray(report.claims) ? (report.claims as AnyObj[]) : [];
|
|
2533
|
-
const reportClaim = reportClaims.find((c: AnyObj) => c.id === claimId);
|
|
2534
|
-
|
|
2535
|
-
if (!reportClaim) {
|
|
2536
|
-
return {
|
|
2537
|
-
found: false,
|
|
2538
|
-
status: "unknown",
|
|
2539
|
-
value: "",
|
|
2540
|
-
claimType: "",
|
|
2541
|
-
evidence: [],
|
|
2542
|
-
policy: null,
|
|
2543
|
-
why: { directInputs: [], leafClaims: [], diagnostics: [], transparencyGaps: [], changeRecords: [] },
|
|
2544
|
-
};
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
const bundleClaims = Array.isArray(bundle.claims) ? (bundle.claims as AnyObj[]) : [];
|
|
2548
|
-
const bundleClaim = bundleClaims.find((c: AnyObj) => c.id === claimId) ?? reportClaim;
|
|
2549
|
-
const bundlePolicies = Array.isArray(bundle.policies) ? (bundle.policies as AnyObj[]) : [];
|
|
2550
|
-
const bundleEvidence = Array.isArray(bundle.evidence) ? (bundle.evidence as AnyObj[]) : [];
|
|
2551
|
-
|
|
2552
|
-
// Governing policy — follow verificationPolicyId into bundle.policies[]
|
|
2553
|
-
const verificationPolicyId = typeof bundleClaim.verificationPolicyId === "string" ? bundleClaim.verificationPolicyId : undefined;
|
|
2554
|
-
const rawPolicy = verificationPolicyId ? bundlePolicies.find((p: AnyObj) => p.id === verificationPolicyId) : undefined;
|
|
2555
|
-
const policy = rawPolicy
|
|
2556
|
-
? {
|
|
2557
|
-
id: String(rawPolicy.id ?? ""),
|
|
2558
|
-
requiredEvidence: Array.isArray(rawPolicy.requiredEvidence) ? (rawPolicy.requiredEvidence as string[]) : [],
|
|
2559
|
-
requiredMethods: Array.isArray(rawPolicy.requiredMethods) ? (rawPolicy.requiredMethods as string[]) : undefined,
|
|
2560
|
-
acceptanceCriteria: Array.isArray(rawPolicy.acceptanceCriteria) ? (rawPolicy.acceptanceCriteria as string[]) : [],
|
|
2561
|
-
reviewAuthority: String(rawPolicy.reviewAuthority ?? ""),
|
|
2562
|
-
}
|
|
2563
|
-
: null;
|
|
2564
|
-
|
|
2565
|
-
// Evidence enhancement: pull evidence items for this claim, surface the execution block
|
|
2566
|
-
const claimEvidenceItems = bundleEvidence.filter((ev: AnyObj) => ev && ev.claimId === claimId);
|
|
2567
|
-
const evidence: ClaimEvidenceItem[] = claimEvidenceItems.map((ev: AnyObj) => {
|
|
2568
|
-
const exec = ev.execution && typeof ev.execution === "object" ? (ev.execution as AnyObj) : null;
|
|
2569
|
-
const execution = exec
|
|
2570
|
-
? {
|
|
2571
|
-
runner: String(exec.runner ?? exec.label ?? ""),
|
|
2572
|
-
label: String(exec.label ?? exec.runner ?? ""),
|
|
2573
|
-
isError: Boolean(exec.isError ?? (typeof exec.exitCode === "number" && exec.exitCode !== 0)),
|
|
2574
|
-
exitCode: typeof exec.exitCode === "number" ? exec.exitCode : null,
|
|
2575
|
-
}
|
|
2576
|
-
: null;
|
|
2577
|
-
return {
|
|
2578
|
-
evidenceType: String(ev.evidenceType ?? ev.type ?? "unknown"),
|
|
2579
|
-
label: String(ev.label ?? ev.excerptOrSummary ?? ev.sourceRef ?? ev.id ?? ""),
|
|
2580
|
-
execution,
|
|
2581
|
-
passing: execution ? !execution.isError : String(ev.status ?? "") !== "disputed",
|
|
2582
|
-
summary: String(ev.excerptOrSummary ?? ev.summary ?? ev.label ?? ""),
|
|
2583
|
-
};
|
|
2584
|
-
});
|
|
2585
|
-
|
|
2586
|
-
// Drilldown: extract from report structure (report.transparencyGaps, report.changeRecords)
|
|
2587
|
-
const allGaps = Array.isArray(report.transparencyGaps) ? (report.transparencyGaps as AnyObj[]) : [];
|
|
2588
|
-
const allChanges = Array.isArray(report.changeRecords) ? (report.changeRecords as AnyObj[]) : [];
|
|
2589
|
-
const transparencyGaps = allGaps.filter((g: AnyObj) => g && g.claimId === claimId);
|
|
2590
|
-
const changeRecords = allChanges.filter((c: AnyObj) => c && c.claimId === claimId);
|
|
2591
|
-
|
|
2592
|
-
return {
|
|
2593
|
-
found: true,
|
|
2594
|
-
status: String(reportClaim.status ?? "unknown"),
|
|
2595
|
-
value: String(bundleClaim.value ?? reportClaim.value ?? ""),
|
|
2596
|
-
claimType: String(bundleClaim.claimType ?? reportClaim.claimType ?? ""),
|
|
2597
|
-
evidence,
|
|
2598
|
-
policy,
|
|
2599
|
-
why: {
|
|
2600
|
-
directInputs: [], // populated by buildDerivationDrilldown if non-leaf
|
|
2601
|
-
leafClaims: [],
|
|
2602
|
-
diagnostics: [],
|
|
2603
|
-
transparencyGaps,
|
|
2604
|
-
changeRecords,
|
|
2605
|
-
},
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2485
|
+
// buildClaimExplanation + its types are extracted to ./sidecar-claim-explain.ts
|
|
2486
|
+
// (ops#22): a PURE projection (report + bundle + id in, structured explanation out)
|
|
2487
|
+
// with no fs/CLI/shared state, unit-tested in isolation. Re-exported here so the
|
|
2488
|
+
// library facade (src/index.ts) and the IO `claimLookup` handler below are unchanged.
|
|
2489
|
+
export { buildClaimExplanation } from "./sidecar-claim-explain.js";
|
|
2490
|
+
export type { ClaimEvidenceItem, ClaimExplanation } from "./sidecar-claim-explain.js";
|
|
2491
|
+
import { buildClaimExplanation } from "./sidecar-claim-explain.js";
|
|
2608
2492
|
|
|
2609
2493
|
/**
|
|
2610
2494
|
* claim <id> <dir>
|
|
@@ -2768,7 +2652,9 @@ Available claim ids:
|
|
|
2768
2652
|
async function main(): Promise<number> {
|
|
2769
2653
|
const p = parseArgs(process.argv.slice(2));
|
|
2770
2654
|
if (!p.command) die("workflow-sidecar command is required");
|
|
2771
|
-
const lockRoot = ["ensure-session", "current", "dogfood-pass", "liveness"].includes(p.command)
|
|
2655
|
+
const lockRoot = ["ensure-session", "current", "dogfood-pass", "liveness"].includes(p.command)
|
|
2656
|
+
? (opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : (p.command === "ensure-session" ? flowAgentsArtifactRoot() : defaultArtifactRootForRead()))
|
|
2657
|
+
: p.command === "record-agent-event" ? explicitArtifactRoot(p) : p.command === "claim" ? (p.positional[1] ? path.resolve(p.positional[1]) : "") : p.positional[0] ? artifactDirFrom(p.positional[0]) : "";
|
|
2772
2658
|
return withLock(lockRoot, ["ensure-session", "record-agent-event", "dogfood-pass"].includes(p.command), p.command, () => {
|
|
2773
2659
|
switch (p.command) {
|
|
2774
2660
|
case "ensure-session": return ensureSession(p);
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,20 @@
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
|
|
18
18
|
|
|
19
|
+
export {
|
|
20
|
+
defaultArtifactRootForRead,
|
|
21
|
+
defaultTelemetryDirForRead,
|
|
22
|
+
defaultTelemetryDirsForRead,
|
|
23
|
+
firstExistingPath,
|
|
24
|
+
flowAgentsArtifactRoot,
|
|
25
|
+
KONTOURAI_DIR,
|
|
26
|
+
legacyFlowAgentsArtifactRoot,
|
|
27
|
+
LEGACY_FLOW_AGENTS_DIR,
|
|
28
|
+
legacyTelemetryDataDir,
|
|
29
|
+
LEGACY_TELEMETRY_DIR,
|
|
30
|
+
telemetryDataDir,
|
|
31
|
+
} from "./lib/local-artifact-root.js";
|
|
32
|
+
|
|
19
33
|
export {
|
|
20
34
|
// Trust-bundle (Hachure) validation — the same validator the writer uses.
|
|
21
35
|
validateTrustBundle,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const KONTOURAI_DIR = ".kontourai";
|
|
5
|
+
export const LEGACY_FLOW_AGENTS_DIR = ".flow-agents";
|
|
6
|
+
export const LEGACY_TELEMETRY_DIR = ".telemetry";
|
|
7
|
+
|
|
8
|
+
export function flowAgentsArtifactRoot(cwd = process.cwd()): string {
|
|
9
|
+
return path.resolve(cwd, KONTOURAI_DIR, "flow-agents");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function legacyFlowAgentsArtifactRoot(cwd = process.cwd()): string {
|
|
13
|
+
return path.resolve(cwd, LEGACY_FLOW_AGENTS_DIR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function telemetryDataDir(cwd = process.cwd()): string {
|
|
17
|
+
return path.resolve(cwd, KONTOURAI_DIR, "telemetry");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function legacyTelemetryDataDir(cwd = process.cwd()): string {
|
|
21
|
+
return path.resolve(cwd, LEGACY_TELEMETRY_DIR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function firstExistingPath(candidates: string[]): string {
|
|
25
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defaultArtifactRootForRead(cwd = process.cwd()): string {
|
|
29
|
+
return firstExistingPath([flowAgentsArtifactRoot(cwd), legacyFlowAgentsArtifactRoot(cwd)]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defaultTelemetryDirForRead(cwd = process.cwd()): string {
|
|
33
|
+
return firstExistingPath([telemetryDataDir(cwd), legacyTelemetryDataDir(cwd)]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function defaultTelemetryDirsForRead(cwd = process.cwd()): string[] {
|
|
37
|
+
const dirs = [telemetryDataDir(cwd), legacyTelemetryDataDir(cwd)];
|
|
38
|
+
return dirs.filter((dir, index) => dirs.indexOf(dir) === index && fs.existsSync(dir));
|
|
39
|
+
}
|
|
@@ -74,6 +74,7 @@ const hookFilePolicies = new Map<string, { category: string; requiredNeedles: st
|
|
|
74
74
|
["scripts/hooks/lib/audit-transport.sh", { category: "shared hook library", requiredNeedles: ["audit_emit"] }],
|
|
75
75
|
["scripts/hooks/lib/hook-flags.js", { category: "shared hook library", requiredNeedles: ["isHookEnabled"] }],
|
|
76
76
|
["scripts/hooks/lib/liveness-read.js", { category: "shared hook library", requiredNeedles: ["freshHolders", "readLivenessEvents"] }],
|
|
77
|
+
["scripts/hooks/lib/local-artifact-paths.js", { category: "shared hook library", requiredNeedles: ["flowAgentsArtifactRoot", "defaultArtifactRootForRead"] }],
|
|
77
78
|
["scripts/hooks/lib/patterns.sh", { category: "shared hook library", requiredNeedles: ["_detect_secrets"] }],
|
|
78
79
|
["scripts/hooks/lib/resolve-formatter.js", { category: "shared hook library", requiredNeedles: ["resolveFormatter"] }],
|
|
79
80
|
]);
|