@kontourai/flow-agents 3.5.0 → 3.7.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 +32 -3
- package/build/src/builder-flow-run-adapter.js +113 -20
- package/build/src/builder-flow-runtime.d.ts +26 -3
- package/build/src/builder-flow-runtime.js +502 -49
- 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 +14 -7
- package/build/src/cli/assignment-provider.js +128 -65
- package/build/src/cli/builder-run.js +46 -9
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +30 -3
- package/build/src/cli/workflow-sidecar.js +825 -99
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +769 -0
- package/build/src/cli.js +2 -0
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +7 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- 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/build/src/tools/generate-context-map.js +5 -3
- package/context/contracts/artifact-contract.md +1 -1
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +79 -10
- package/docs/context-map.md +24 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/public-workflow-cli.md +132 -0
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +78 -40
- package/docs/workflow-usage-guide.md +110 -38
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +57 -45
- package/evals/integration/test_builder_step_producers.sh +297 -6
- package/evals/integration/test_bundle_install.sh +258 -55
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +573 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- 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/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +79 -10
- package/src/builder-flow-run-adapter.ts +156 -23
- package/src/builder-flow-runtime.ts +535 -53
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +91 -22
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +597 -8
- package/src/cli/builder-run.ts +54 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +748 -99
- package/src/cli/workflow.ts +708 -0
- package/src/cli.ts +2 -0
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +20 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/lib/package-version.ts +15 -0
- package/src/lib/pinned-cli-command.ts +23 -0
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -7,22 +7,39 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
10
|
-
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
|
|
10
|
+
import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolveFlowStep, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
|
|
11
11
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
12
12
|
import { ensureSafeDirectory } from "../lib/fs.js";
|
|
13
|
-
import {
|
|
13
|
+
import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
|
|
14
|
+
import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
|
|
15
|
+
import { runObservedCommand } from "../lib/observed-command.js";
|
|
16
|
+
import { captureReviewWorkspaceSnapshot, startBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
|
|
14
17
|
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
15
18
|
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
16
19
|
// `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
|
|
17
20
|
// import — same idiom already used above for ../lib/flow-resolver.js).
|
|
18
21
|
import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus, withSubjectLock } from "./assignment-provider.js";
|
|
19
|
-
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
22
|
+
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "canceled", "accepted", "archived"]);
|
|
20
23
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
21
24
|
export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
22
25
|
export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
|
|
23
26
|
export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
|
|
27
|
+
export const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
|
|
28
|
+
const PUBLIC_WORKFLOW_AUTHORITY = Symbol("public-workflow-authority");
|
|
24
29
|
function now() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
25
30
|
function read(file) { return fs.readFileSync(file, "utf8"); }
|
|
31
|
+
function readRegularFileNoFollow(file, label) {
|
|
32
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
33
|
+
try {
|
|
34
|
+
const stat = fs.fstatSync(fd);
|
|
35
|
+
if (!stat.isFile())
|
|
36
|
+
die(`${label} must be a regular file`);
|
|
37
|
+
return fs.readFileSync(fd);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
export function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
|
|
27
44
|
// Single-line but readable "key": "value" form. Built by collapsing the
|
|
28
45
|
// structural whitespace from an indented stringify — corruption-proof, unlike a
|
|
@@ -44,8 +61,14 @@ function slugify(value, fallback) { return value.toLowerCase().replace(/[^a-z0-9
|
|
|
44
61
|
* Reuses slugify() for normalization. Validates that the id is a numeric GitHub issue number. */
|
|
45
62
|
function workItemSlug(ref) {
|
|
46
63
|
const hashIdx = ref.indexOf("#");
|
|
47
|
-
if (hashIdx < 0
|
|
48
|
-
|
|
64
|
+
if (hashIdx < 0) {
|
|
65
|
+
if (!/^[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(ref) || ref.includes("..")) {
|
|
66
|
+
die("--work-item must be a provider-neutral provider:id ref or owner/repo#numeric-id");
|
|
67
|
+
}
|
|
68
|
+
return slugify(ref, "work-item");
|
|
69
|
+
}
|
|
70
|
+
if (hashIdx === ref.length - 1)
|
|
71
|
+
die("--work-item must be in owner/repo#numeric-id format");
|
|
49
72
|
const repoPath = ref.slice(0, hashIdx);
|
|
50
73
|
const id = ref.slice(hashIdx + 1);
|
|
51
74
|
if (!/^\d+$/.test(id))
|
|
@@ -59,9 +82,12 @@ function workItemSlug(ref) {
|
|
|
59
82
|
function assignmentSubjectMatchesWorkItem(slug, ref) {
|
|
60
83
|
if (ref === `local:${slug}`)
|
|
61
84
|
return true;
|
|
62
|
-
|
|
85
|
+
try {
|
|
86
|
+
return workItemSlug(ref) === slug;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
63
89
|
return false;
|
|
64
|
-
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
function sessionWorkItem(p, slug, dir) {
|
|
67
93
|
const providerRef = opt(p, "work-item");
|
|
@@ -584,10 +610,10 @@ export function reduceCaptureLogByCommand(commandLog) {
|
|
|
584
610
|
* @param timestamp ISO-8601 timestamp for createdAt / updatedAt / observedAt
|
|
585
611
|
* @param checks Normalized check objects (from record-evidence --check-json / --surface-trust-json)
|
|
586
612
|
* @param criteria Acceptance criteria objects (from acceptance.json .criteria array)
|
|
587
|
-
* @param critiques Critique objects
|
|
613
|
+
* @param critiques Critique objects reconstructed from trust.bundle claims
|
|
588
614
|
* @param commandLog Optional parsed command-log.jsonl entries (capture-authoritative fold)
|
|
589
615
|
*/
|
|
590
|
-
export async function buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, flowAgentsDir, actorKey) {
|
|
616
|
+
export async function buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, flowAgentsDir, actorKey, exactFlowContext) {
|
|
591
617
|
const surface = await tryLoadSurface();
|
|
592
618
|
if (!surface)
|
|
593
619
|
return null;
|
|
@@ -600,7 +626,10 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
600
626
|
// #291 Wave 2 Task 2.1 (§7)/Task 2.2: actorKey (when the caller already resolved one — see
|
|
601
627
|
// writeTrustBundle below) threads through to resolveActiveFlowStep's per-actor-first,
|
|
602
628
|
// legacy-fallback current.json read; omitted, this is IDENTICAL to pre-#291 behavior.
|
|
603
|
-
const
|
|
629
|
+
const exactRepoRoot = flowAgentsDir ? findRepoRootFromDir(path.dirname(flowAgentsDir)) : null;
|
|
630
|
+
const activeStep = exactFlowContext && exactRepoRoot
|
|
631
|
+
? resolveFlowStep(exactFlowContext.flowId, exactFlowContext.stepId, exactRepoRoot)
|
|
632
|
+
: flowAgentsDir ? resolveActiveFlowStep(flowAgentsDir, actorKey) : null;
|
|
604
633
|
// #270 CRITICAL/HIGH fix: resolve the session's active_flow_id independent of whether the
|
|
605
634
|
// CURRENTLY-active step resolves — a stamped gate claim names the STEP IT WAS ORIGINALLY
|
|
606
635
|
// RECORDED AT (frozen in metadata.gate_claim.step_id), which is routinely a DIFFERENT step than
|
|
@@ -609,6 +638,8 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
609
638
|
// the gate claim was recorded at — the validation must be against the FULL flow definition
|
|
610
639
|
// (every step's gate expects[], via resolveAllFlowGateExpects), not just the active one.
|
|
611
640
|
const sessionFlowId = (() => {
|
|
641
|
+
if (exactFlowContext)
|
|
642
|
+
return exactFlowContext.flowId;
|
|
612
643
|
if (!flowAgentsDir)
|
|
613
644
|
return null;
|
|
614
645
|
try {
|
|
@@ -899,6 +930,11 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
899
930
|
const outputDigestMeta = typeof check._output_sha256 === "string" && check._output_sha256.length > 0
|
|
900
931
|
? { algorithm: "sha256", hex: check._output_sha256 }
|
|
901
932
|
: null;
|
|
933
|
+
const observedCommandsMeta = Array.isArray(check._observed_commands)
|
|
934
|
+
? check._observed_commands
|
|
935
|
+
.filter((entry) => typeof entry?.command === "string" && typeof entry?.exit_code === "number" && typeof entry?.output_sha256 === "string")
|
|
936
|
+
.map((entry) => ({ command: entry.command, exit_code: entry.exit_code, output_sha256: entry.output_sha256, ...(Number.isSafeInteger(entry.test_count) ? { test_count: entry.test_count } : {}), ...(entry.execution_proof && typeof entry.execution_proof === "object" ? { execution_proof: entry.execution_proof } : {}) }))
|
|
937
|
+
: null;
|
|
902
938
|
// #270(a)/(c): a gate claim's declared claimType/subjectType, once resolved (either freshly
|
|
903
939
|
// via matchExpectsEntry below, or restored from a prior write's metadata.gate_claim stamp by
|
|
904
940
|
// checksFromBundle), is stamped here so it is frozen at record time — a later bundle rebuild
|
|
@@ -928,11 +964,15 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
928
964
|
origin: "check",
|
|
929
965
|
check_kind: String(check.kind ?? "external"),
|
|
930
966
|
...(activeStep && workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}),
|
|
967
|
+
...(typeof check._producer === "string" ? { expected_producer: check._producer } : {}),
|
|
968
|
+
...(typeof check._recorded_by === "string" ? { recorded_by: check._recorded_by } : {}),
|
|
969
|
+
...(Array.isArray(check._producer_self_produced_trust_slices) ? { self_produced_trust_slices: check._producer_self_produced_trust_slices } : {}),
|
|
931
970
|
...(waiver ? { waiver } : {}),
|
|
932
971
|
...(promotionMeta ? { promotion: promotionMeta } : {}),
|
|
933
972
|
...(artifactRefsMeta ? { artifact_refs: artifactRefsMeta } : {}),
|
|
934
973
|
...(standardRefsMeta ? { standard_refs: standardRefsMeta } : {}),
|
|
935
974
|
...(outputDigestMeta ? { output_digest: outputDigestMeta } : {}),
|
|
975
|
+
...(observedCommandsMeta && observedCommandsMeta.length > 0 ? { observed_commands: observedCommandsMeta } : {}),
|
|
936
976
|
};
|
|
937
977
|
const claimEvents = [];
|
|
938
978
|
if (evStatus) {
|
|
@@ -1028,13 +1068,13 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1028
1068
|
if (declared) {
|
|
1029
1069
|
// Declared kit-typed claim only — no legacy shadow (ADR 0016 P-d).
|
|
1030
1070
|
const declaredPolicy = ensurePolicy(declared.claimType, "high", []);
|
|
1031
|
-
const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1071
|
+
const declaredClaimObj = { id: claimId, subjectType: declared.subjectType, subjectId, facet: "flow-agents.workflow", claimType: declared.claimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: declaredPolicy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [] }, ...(workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}) } };
|
|
1032
1072
|
const { status: declaredStatus } = deriveClaimStatus({ claim: declaredClaimObj, evidence: [], events: claimEvents, policies: [declaredPolicy] });
|
|
1033
1073
|
claims.push({ ...declaredClaimObj, status: declaredStatus });
|
|
1034
1074
|
}
|
|
1035
1075
|
else {
|
|
1036
1076
|
// No active flow step — only the workflow.* primary claim (legitimate no-flow fallback path).
|
|
1037
|
-
const claimObj = { id: claimId, subjectType: "workflow-acceptance-criterion", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: policy.id, metadata: { origin: "acceptance" } };
|
|
1077
|
+
const claimObj = { id: claimId, subjectType: "workflow-acceptance-criterion", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: criterion.status, createdAt: ts, updatedAt: ts, impactLevel: "high", verificationPolicyId: policy.id, metadata: { origin: "acceptance", criterion: { id: criterion.id, description: criterion.description ?? criterion.id, status: criterion.status, evidence_refs: Array.isArray(criterion.evidence_refs) ? criterion.evidence_refs : [] } } };
|
|
1038
1078
|
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [policy] });
|
|
1039
1079
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
1040
1080
|
}
|
|
@@ -1052,7 +1092,18 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1052
1092
|
const supersededBy = typeof c.superseded_by === "string" && c.superseded_by.length > 0 ? c.superseded_by : null;
|
|
1053
1093
|
const critiqueReviewer = String(c.reviewer ?? "tool-code-reviewer");
|
|
1054
1094
|
const critiqueReviewedAt = String(c.reviewed_at ?? ts);
|
|
1055
|
-
const critMeta = {
|
|
1095
|
+
const critMeta = {
|
|
1096
|
+
origin: "critique",
|
|
1097
|
+
reviewer: critiqueReviewer,
|
|
1098
|
+
reviewed_at: critiqueReviewedAt,
|
|
1099
|
+
findings: Array.isArray(c.findings) ? c.findings : [],
|
|
1100
|
+
lanes: Array.isArray(c.lanes) ? c.lanes : [],
|
|
1101
|
+
review_target: c.review_target && typeof c.review_target === "object" ? c.review_target : { artifacts: [] },
|
|
1102
|
+
// Keep the legacy field for consumers that still render a flat artifact list.
|
|
1103
|
+
artifact_refs: Array.isArray(c.artifact_refs) ? c.artifact_refs : [],
|
|
1104
|
+
...(activeStep && workflowSubjectRef ? { workflow_subject_ref: workflowSubjectRef } : {}),
|
|
1105
|
+
...(supersededBy ? { superseded_by: supersededBy } : {}),
|
|
1106
|
+
};
|
|
1056
1107
|
// A superseded historical write gets a distinct, stable claimId so it co-exists with the live
|
|
1057
1108
|
// claim of the same critique id (never overwrites or duplicates it). The salt is reproducible
|
|
1058
1109
|
// across rebuilds because superseded_by + reviewed_at are preserved in metadata.
|
|
@@ -1068,18 +1119,15 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1068
1119
|
events.push(evt);
|
|
1069
1120
|
claimEvents.push(evt);
|
|
1070
1121
|
}
|
|
1071
|
-
//
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
const subjectType = declared ? declared.subjectType : "workflow-critique";
|
|
1075
|
-
const claimPolicy = declared ? ensurePolicy(declared.claimType, "medium", []) : policy;
|
|
1076
|
-
const claimObj = { id: claimId, subjectType, subjectId, facet: "flow-agents.workflow", claimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: claimPolicy.id, metadata: critMeta };
|
|
1122
|
+
// Critique is intentionally report-only. It must never inherit a Flow gate expectation,
|
|
1123
|
+
// even while a run is positioned at a gate-bearing step.
|
|
1124
|
+
const claimObj = { id: claimId, subjectType: "workflow-critique", subjectId, facet: "flow-agents.workflow", claimType: legacyClaimType, fieldOrBehavior, value: c.verdict, createdAt: ts, updatedAt: ts, impactLevel: "medium", verificationPolicyId: policy.id, metadata: critMeta };
|
|
1077
1125
|
if (supersededBy) {
|
|
1078
1126
|
// History: status is "superseded" directly (no verification event); excluded from evaluation.
|
|
1079
1127
|
claims.push({ ...claimObj, status: "superseded" });
|
|
1080
1128
|
}
|
|
1081
1129
|
else {
|
|
1082
|
-
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [
|
|
1130
|
+
const { status: derivedStatus } = deriveClaimStatus({ claim: claimObj, evidence: [], events: claimEvents, policies: [policy] });
|
|
1083
1131
|
claims.push({ ...claimObj, status: derivedStatus });
|
|
1084
1132
|
}
|
|
1085
1133
|
}
|
|
@@ -1105,7 +1153,7 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
1105
1153
|
* @param criteria Acceptance criteria objects (same as buildTrustBundle)
|
|
1106
1154
|
* @param critiques Critique objects (same as buildTrustBundle)
|
|
1107
1155
|
*/
|
|
1108
|
-
export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, critiques, actorKey) {
|
|
1156
|
+
export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, critiques, actorKey, exactFlowContext) {
|
|
1109
1157
|
try {
|
|
1110
1158
|
// Fold the deterministic capture log (PostToolUse evidence-capture) into the
|
|
1111
1159
|
// bundle so capture is authoritative over claimed status. Best-effort read.
|
|
@@ -1131,15 +1179,22 @@ export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, c
|
|
|
1131
1179
|
const _flowAgentsDir = path.dirname(dir);
|
|
1132
1180
|
const _effectiveActorKey = actorKey ?? loadActorIdentityHelper().resolveActor(process.env).actor;
|
|
1133
1181
|
let _scopedFlowAgentsDir = undefined;
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1182
|
+
if (exactFlowContext) {
|
|
1183
|
+
// The context was read from this session's persisted flow_run. Navigation pointers may
|
|
1184
|
+
// lag or point at another run and must not override an explicit session mutation.
|
|
1185
|
+
_scopedFlowAgentsDir = _flowAgentsDir;
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
try {
|
|
1189
|
+
const _currentRaw = loadCurrentPointerHelper().readCurrentPointer(_flowAgentsDir, _effectiveActorKey).payload;
|
|
1190
|
+
const _artDir = _currentRaw && typeof _currentRaw["artifact_dir"] === "string" ? _currentRaw["artifact_dir"] : null;
|
|
1191
|
+
if (_artDir && path.resolve(_flowAgentsDir, _artDir) === path.resolve(dir)) {
|
|
1192
|
+
_scopedFlowAgentsDir = _flowAgentsDir;
|
|
1193
|
+
}
|
|
1139
1194
|
}
|
|
1195
|
+
catch { /* current.json absent or unreadable — no scoping */ }
|
|
1140
1196
|
}
|
|
1141
|
-
|
|
1142
|
-
const bundle = await buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, _scopedFlowAgentsDir, _effectiveActorKey);
|
|
1197
|
+
const bundle = await buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, _scopedFlowAgentsDir, _effectiveActorKey, exactFlowContext);
|
|
1143
1198
|
if (!bundle)
|
|
1144
1199
|
return { written: false, errors: [] }; // Surface unavailable — fail-open, skip write
|
|
1145
1200
|
const result = await validateTrustBundle(bundle);
|
|
@@ -1148,7 +1203,6 @@ export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, c
|
|
|
1148
1203
|
return { written: false, errors: result.errors };
|
|
1149
1204
|
}
|
|
1150
1205
|
writeJson(path.join(dir, "trust.bundle"), bundle);
|
|
1151
|
-
await syncBuilderFlowSessionIfPresent(dir);
|
|
1152
1206
|
return { written: true, errors: [] };
|
|
1153
1207
|
}
|
|
1154
1208
|
catch (err) {
|
|
@@ -1681,6 +1735,10 @@ function currentDir(root, actorKey) {
|
|
|
1681
1735
|
}
|
|
1682
1736
|
return dir;
|
|
1683
1737
|
}
|
|
1738
|
+
export function currentWorkflowSessionDir(root) {
|
|
1739
|
+
const resolved = loadActorIdentityHelper().resolveActor(process.env);
|
|
1740
|
+
return currentDir(path.resolve(root), loadActorIdentityHelper().isUnresolvedActor(resolved.actor) ? undefined : resolved.actor);
|
|
1741
|
+
}
|
|
1684
1742
|
/**
|
|
1685
1743
|
* #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
|
|
1686
1744
|
* exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
|
|
@@ -1754,6 +1812,9 @@ function initSidecars(dir, slug, sourceRequest, summary, nextAction, timestamp,
|
|
|
1754
1812
|
...sidecarBase(slug), status: initialLifecycle?.status ?? "planned", phase: initialLifecycle?.phase ?? "planning", created_at: existingState.created_at || timestamp, updated_at: timestamp,
|
|
1755
1813
|
...(branch ? { branch } : {}),
|
|
1756
1814
|
...(retainedWorkItemRefs.length > 0 ? { work_item_refs: retainedWorkItemRefs } : {}),
|
|
1815
|
+
...(existingState.flow_run && typeof existingState.flow_run === "object" && !Array.isArray(existingState.flow_run)
|
|
1816
|
+
? { flow_run: existingState.flow_run }
|
|
1817
|
+
: {}),
|
|
1757
1818
|
artifact_paths: relArtifacts(dir),
|
|
1758
1819
|
next_action: nextActionPayload,
|
|
1759
1820
|
});
|
|
@@ -1830,14 +1891,40 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemR
|
|
|
1830
1891
|
const nowMs = opt(p, "now") ? Date.parse(opt(p, "now")) : Date.now();
|
|
1831
1892
|
const assignmentProviderKind = opt(p, "assignment-provider", "local-file");
|
|
1832
1893
|
let effective;
|
|
1894
|
+
let providerEvidenceBytes;
|
|
1833
1895
|
const effectiveStateJsonFlag = opt(p, "effective-state-json");
|
|
1834
1896
|
if (effectiveStateJsonFlag) {
|
|
1835
|
-
|
|
1897
|
+
providerEvidenceBytes = effectiveStateJsonFlag === "-"
|
|
1898
|
+
? fs.readFileSync(0)
|
|
1899
|
+
: readRegularFileNoFollow(path.resolve(effectiveStateJsonFlag), "ensure-session --effective-state-json");
|
|
1900
|
+
const parsed = JSON.parse(providerEvidenceBytes.toString("utf8"));
|
|
1836
1901
|
const candidate = parsed && typeof parsed === "object" ? parsed.effective : undefined;
|
|
1902
|
+
const assignment = parsed && typeof parsed === "object" ? parsed.assignment : undefined;
|
|
1903
|
+
const record = assignment && typeof assignment.record === "object" && assignment.record !== null ? assignment.record : undefined;
|
|
1837
1904
|
const validStates = new Set(["held", "reclaimable", "human-held", "free"]);
|
|
1838
1905
|
if (!candidate || typeof candidate.effective_state !== "string" || !validStates.has(candidate.effective_state)) {
|
|
1839
1906
|
die(`ensure-session --effective-state-json must contain an .effective object with a recognized effective_state (held|reclaimable|human-held|free); got ${JSON.stringify(candidate ? candidate.effective_state : candidate)}`);
|
|
1840
1907
|
}
|
|
1908
|
+
if (workItemRef && (parsed.role !== "AssignmentStatus"
|
|
1909
|
+
|| parsed.provider !== assignmentProviderKind
|
|
1910
|
+
|| assignment?.provider !== assignmentProviderKind
|
|
1911
|
+
|| assignment?.subject_id !== slug
|
|
1912
|
+
|| record?.role !== "AssignmentClaimRecord"
|
|
1913
|
+
|| record?.status !== "claimed"
|
|
1914
|
+
|| record?.subject_id !== slug
|
|
1915
|
+
|| record?.work_item_ref !== workItemRef
|
|
1916
|
+
|| record?.actor_key !== resolution.branchActorKey
|
|
1917
|
+
|| assignment?.assignee !== resolution.branchActorKey
|
|
1918
|
+
|| !record.actor || typeof record.actor !== "object"
|
|
1919
|
+
|| record.actor.runtime !== resolution.actorStruct.runtime
|
|
1920
|
+
|| record.actor.session_id !== resolution.actorStruct.session_id
|
|
1921
|
+
|| record.actor.host !== resolution.actorStruct.host
|
|
1922
|
+
|| (record.actor.human ?? null) !== (resolution.actorStruct.human ?? null)
|
|
1923
|
+
|| candidate.effective_state !== "held"
|
|
1924
|
+
|| candidate.reason !== "self_is_holder"
|
|
1925
|
+
|| candidate.holder?.actor !== resolution.branchActorKey)) {
|
|
1926
|
+
die("ensure-session --effective-state-json must be the configured provider's AssignmentStatus for this Work Item, with a claimed record and self_is_holder actor matching the current runtime");
|
|
1927
|
+
}
|
|
1841
1928
|
effective = candidate;
|
|
1842
1929
|
}
|
|
1843
1930
|
else if (assignmentProviderKind === "local-file") {
|
|
@@ -1875,9 +1962,7 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemR
|
|
|
1875
1962
|
return null;
|
|
1876
1963
|
}
|
|
1877
1964
|
const selectedWorkEvidence = () => {
|
|
1878
|
-
|
|
1879
|
-
// can authorize entry, but it cannot prove that this process acquired the Work Item.
|
|
1880
|
-
if (!workItemRef || effectiveStateJsonFlag || assignmentProviderKind !== "local-file")
|
|
1965
|
+
if (!workItemRef)
|
|
1881
1966
|
return null;
|
|
1882
1967
|
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1883
1968
|
const record = assignment.record;
|
|
@@ -1891,6 +1976,8 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemR
|
|
|
1891
1976
|
return {
|
|
1892
1977
|
assignmentFile: assignmentFilePath(root, slug),
|
|
1893
1978
|
actorKey: resolution.branchActorKey,
|
|
1979
|
+
...(effectiveStateJsonFlag ? { providerEvidenceFile: path.resolve(effectiveStateJsonFlag) } : {}),
|
|
1980
|
+
...(providerEvidenceBytes ? { providerEvidenceBytes } : {}),
|
|
1894
1981
|
};
|
|
1895
1982
|
};
|
|
1896
1983
|
const resolveBranchForClaim = () => {
|
|
@@ -1904,8 +1991,22 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemR
|
|
|
1904
1991
|
// ActorStruct triple), so this redundant belt-and-suspenders check agrees with the
|
|
1905
1992
|
// effective_state computation instead of silently using a different identity.
|
|
1906
1993
|
const isSelf = effective.reason === "self_is_holder" || (!!effective.holder?.actor && effective.holder.actor === resolution.branchActorKey);
|
|
1907
|
-
if (isSelf)
|
|
1994
|
+
if (isSelf) {
|
|
1995
|
+
if (assignmentProviderKind !== "local-file") {
|
|
1996
|
+
const local = readLocalAssignmentStatus(root, slug).record;
|
|
1997
|
+
if (!local || local.status !== "claimed") {
|
|
1998
|
+
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
1999
|
+
ttlSeconds: opt(p, "claim-ttl-seconds") ? Number(opt(p, "claim-ttl-seconds")) : loadLivenessPolicyHelper().resolveTtlSeconds(process.env),
|
|
2000
|
+
branch: resolveBranchForClaim(),
|
|
2001
|
+
artifactDir: path.relative(root, dir) || ".",
|
|
2002
|
+
reason: `provider-confirmed ${assignmentProviderKind} ownership mirror`,
|
|
2003
|
+
actorKey: resolution.branchActorKey,
|
|
2004
|
+
...(workItemRef ? { workItemRef } : {}),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1908
2008
|
return selectedWorkEvidence();
|
|
2009
|
+
}
|
|
1909
2010
|
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1910
2011
|
const lastAtSuffix = effective.holder?.last_at ? ` (last_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1911
2012
|
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.`);
|
|
@@ -1957,6 +2058,9 @@ function enforceEnsureSessionOwnership(p, root, slug, dir, resolution, workItemR
|
|
|
1957
2058
|
return selectedWorkEvidence();
|
|
1958
2059
|
}
|
|
1959
2060
|
case "free": {
|
|
2061
|
+
if (assignmentProviderKind !== "local-file") {
|
|
2062
|
+
die(`ensure-session refused: provider ${JSON.stringify(assignmentProviderKind)} has not confirmed this actor as the Work Item holder`);
|
|
2063
|
+
}
|
|
1960
2064
|
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
1961
2065
|
ttlSeconds: opt(p, "claim-ttl-seconds") ? Number(opt(p, "claim-ttl-seconds")) : loadLivenessPolicyHelper().resolveTtlSeconds(process.env),
|
|
1962
2066
|
branch: resolveBranchForClaim(),
|
|
@@ -2008,11 +2112,11 @@ function resolveEnsureSessionEntry(p, dir) {
|
|
|
2008
2112
|
}
|
|
2009
2113
|
return { flowId, stepId: explicitStep || firstStep, firstStep };
|
|
2010
2114
|
}
|
|
2011
|
-
function assertCanonicalBuilderArtifactRoot(root) {
|
|
2115
|
+
function assertCanonicalBuilderArtifactRoot(root, flowId) {
|
|
2012
2116
|
const kontouraiRoot = path.dirname(root);
|
|
2013
2117
|
const projectRoot = path.dirname(kontouraiRoot);
|
|
2014
2118
|
if (path.basename(root) !== "flow-agents" || path.basename(kontouraiRoot) !== ".kontourai") {
|
|
2015
|
-
die(
|
|
2119
|
+
die(`ensure-session --flow-id ${flowId} requires --artifact-root <project>/.kontourai/flow-agents`);
|
|
2016
2120
|
}
|
|
2017
2121
|
const statIfPresent = (candidate) => {
|
|
2018
2122
|
try {
|
|
@@ -2026,7 +2130,7 @@ function assertCanonicalBuilderArtifactRoot(root) {
|
|
|
2026
2130
|
};
|
|
2027
2131
|
const projectStat = statIfPresent(projectRoot);
|
|
2028
2132
|
if (projectStat && (projectStat.isSymbolicLink() || !projectStat.isDirectory())) {
|
|
2029
|
-
die(`ensure-session --flow-id
|
|
2133
|
+
die(`ensure-session --flow-id ${flowId} requires a non-symlink project root: ${projectRoot}`);
|
|
2030
2134
|
}
|
|
2031
2135
|
const realProjectRoot = projectStat ? fs.realpathSync(projectRoot) : projectRoot;
|
|
2032
2136
|
for (const [candidate, expected, label] of [
|
|
@@ -2037,7 +2141,7 @@ function assertCanonicalBuilderArtifactRoot(root) {
|
|
|
2037
2141
|
if (!stat)
|
|
2038
2142
|
continue;
|
|
2039
2143
|
if (stat.isSymbolicLink() || !stat.isDirectory() || fs.realpathSync(candidate) !== expected) {
|
|
2040
|
-
die(`ensure-session --flow-id
|
|
2144
|
+
die(`ensure-session --flow-id ${flowId} requires a non-symlink ${label}: ${candidate}`);
|
|
2041
2145
|
}
|
|
2042
2146
|
}
|
|
2043
2147
|
}
|
|
@@ -2047,18 +2151,18 @@ function preflightEnsureSession(p) {
|
|
|
2047
2151
|
const dir = sessionDirFor(root, slug);
|
|
2048
2152
|
assertSafeSessionDirectory(root, dir);
|
|
2049
2153
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2050
|
-
if (entry.flowId === "builder.build")
|
|
2051
|
-
assertCanonicalBuilderArtifactRoot(root);
|
|
2154
|
+
if (entry.flowId === "builder.build" || entry.flowId === "builder.shape")
|
|
2155
|
+
assertCanonicalBuilderArtifactRoot(root, entry.flowId);
|
|
2052
2156
|
sessionWorkItem(p, slug, dir);
|
|
2053
2157
|
}
|
|
2054
|
-
async function ensureSession(p) {
|
|
2158
|
+
async function ensureSession(p, allowCanonicalFlowMutation) {
|
|
2055
2159
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
2056
2160
|
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)"));
|
|
2057
2161
|
const dir = sessionDirFor(root, slug);
|
|
2058
2162
|
assertSafeSessionDirectory(root, dir);
|
|
2059
2163
|
const entry = resolveEnsureSessionEntry(p, dir);
|
|
2060
|
-
if (entry.flowId === "builder.build")
|
|
2061
|
-
assertCanonicalBuilderArtifactRoot(root);
|
|
2164
|
+
if (entry.flowId === "builder.build" || entry.flowId === "builder.shape")
|
|
2165
|
+
assertCanonicalBuilderArtifactRoot(root, entry.flowId);
|
|
2062
2166
|
const workItem = sessionWorkItem(p, slug, dir);
|
|
2063
2167
|
// #291 Wave 2 Task 2.1 (§3, §4): resolve the actor ONCE, then run the ownership guard BEFORE
|
|
2064
2168
|
// any directory/file is created — a refusal must never leave a stray empty session dir. Reused
|
|
@@ -2068,8 +2172,20 @@ async function ensureSession(p) {
|
|
|
2068
2172
|
const selectionWorkItemRef = entry.flowId === "builder.build" && assignmentSubjectMatchesWorkItem(slug, workItem.ref)
|
|
2069
2173
|
? workItem.ref
|
|
2070
2174
|
: undefined;
|
|
2071
|
-
|
|
2175
|
+
let selectedWorkEvidence = enforceEnsureSessionOwnership(p, root, slug, dir, actorResolution, selectionWorkItemRef);
|
|
2072
2176
|
ensureSafeDirectory(root, dir);
|
|
2177
|
+
if (selectedWorkEvidence?.providerEvidenceBytes) {
|
|
2178
|
+
const staged = path.join(dir, "assignment-provider-state.json");
|
|
2179
|
+
if (fs.existsSync(staged)) {
|
|
2180
|
+
if (!readRegularFileNoFollow(staged, "ensure-session provider state evidence destination").equals(selectedWorkEvidence.providerEvidenceBytes))
|
|
2181
|
+
die("ensure-session provider state evidence conflicts with the existing immutable snapshot");
|
|
2182
|
+
}
|
|
2183
|
+
else {
|
|
2184
|
+
fs.writeFileSync(staged, selectedWorkEvidence.providerEvidenceBytes, { flag: "wx", mode: 0o600 });
|
|
2185
|
+
}
|
|
2186
|
+
fs.chmodSync(staged, 0o400);
|
|
2187
|
+
selectedWorkEvidence = { ...selectedWorkEvidence, providerEvidenceFile: staged };
|
|
2188
|
+
}
|
|
2073
2189
|
const timestamp = opt(p, "timestamp", now());
|
|
2074
2190
|
if (workItem.localRecord && !fs.existsSync(path.join(dir, "work-item.json"))) {
|
|
2075
2191
|
writeJson(path.join(dir, "work-item.json"), workItem.localRecord);
|
|
@@ -2082,13 +2198,27 @@ async function ensureSession(p) {
|
|
|
2082
2198
|
// takeover continuity true by construction (see Design Decision 3 in the plan).
|
|
2083
2199
|
const branch = resolveSessionBranch(p, slug);
|
|
2084
2200
|
const initialMarkdownStatus = entry.flowId ? "new" : "planning";
|
|
2085
|
-
|
|
2201
|
+
const acceptanceCriteria = opts(p, "criterion");
|
|
2202
|
+
if (acceptanceCriteria.length === 0)
|
|
2203
|
+
acceptanceCriteria.push(`Complete the requested outcome: ${opt(p, "summary", "Workflow session is durable.")}`);
|
|
2204
|
+
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`;
|
|
2086
2205
|
fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), md);
|
|
2087
2206
|
}
|
|
2088
2207
|
if (!fs.existsSync(path.join(dir, "state.json")) || !fs.existsSync(path.join(dir, "acceptance.json")) || !fs.existsSync(path.join(dir, "handoff.json"))) {
|
|
2089
2208
|
const phaseMap = entry.flowId ? resolvePhaseMap(entry.flowId, findRepoRootFromDir(dir)) : null;
|
|
2090
2209
|
const initialPhase = Object.entries(phaseMap ?? {}).find(([, step]) => step === entry.firstStep)?.[0];
|
|
2091
|
-
const startCommand =
|
|
2210
|
+
const startCommand = pinnedFlowAgentsCommand(flowAgentsPackageVersion(), [
|
|
2211
|
+
"workflow", "start",
|
|
2212
|
+
"--flow", entry.flowId,
|
|
2213
|
+
...(entry.flowId === "builder.shape" ? [] : ["--work-item", workItem.ref]),
|
|
2214
|
+
...(workItem.ref === `local:${slug}` ? ["--task-slug", slug] : []),
|
|
2215
|
+
"--assignment-provider", opt(p, "assignment-provider", "local-file"),
|
|
2216
|
+
...(selectedWorkEvidence?.providerEvidenceFile ? ["--effective-state-json", selectedWorkEvidence.providerEvidenceFile] : []),
|
|
2217
|
+
"--artifact-root", root,
|
|
2218
|
+
"--title", opt(p, "title", slug),
|
|
2219
|
+
"--summary", opt(p, "summary", "Workflow session is durable."),
|
|
2220
|
+
...opts(p, "criterion").flatMap((criterion) => ["--criterion", criterion]),
|
|
2221
|
+
]);
|
|
2092
2222
|
const nextAction = entry.flowId
|
|
2093
2223
|
? entry.flowId === "builder.build"
|
|
2094
2224
|
? {
|
|
@@ -2116,9 +2246,9 @@ async function ensureSession(p) {
|
|
|
2116
2246
|
? persistedCurrent.active_step_id
|
|
2117
2247
|
: entry.stepId;
|
|
2118
2248
|
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", entry.flowId || undefined, resumedStep || undefined, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
2119
|
-
if (entry.flowId === "builder.build") {
|
|
2249
|
+
if (allowCanonicalFlowMutation && (entry.flowId === "builder.build" || entry.flowId === "builder.shape")) {
|
|
2120
2250
|
try {
|
|
2121
|
-
const started = await startBuilderFlowSession({ sessionDir: dir });
|
|
2251
|
+
const started = await startBuilderFlowSession({ sessionDir: dir, flowId: entry.flowId });
|
|
2122
2252
|
if (started.run.state.current_step === "pull-work"
|
|
2123
2253
|
&& selectedWorkEvidence
|
|
2124
2254
|
&& assignmentSubjectMatchesWorkItem(slug, workItem.ref)) {
|
|
@@ -2133,8 +2263,25 @@ async function ensureSession(p) {
|
|
|
2133
2263
|
}
|
|
2134
2264
|
const assignmentContent = fs.readFileSync(selectedWorkEvidence.assignmentFile);
|
|
2135
2265
|
const assignmentDigest = createHash("sha256").update(assignmentContent).digest("hex");
|
|
2266
|
+
const providerEvidenceBytes = selectedWorkEvidence.providerEvidenceFile
|
|
2267
|
+
? readRegularFileNoFollow(selectedWorkEvidence.providerEvidenceFile, "selected-work provider evidence")
|
|
2268
|
+
: null;
|
|
2269
|
+
if (providerEvidenceBytes && selectedWorkEvidence.providerEvidenceBytes && !providerEvidenceBytes.equals(selectedWorkEvidence.providerEvidenceBytes)) {
|
|
2270
|
+
die("ensure-session refused to emit selected-work evidence: provider evidence changed after ownership validation");
|
|
2271
|
+
}
|
|
2272
|
+
const providerEvidenceDigest = providerEvidenceBytes ? createHash("sha256").update(providerEvidenceBytes).digest("hex") : null;
|
|
2136
2273
|
const projectRoot = path.dirname(path.dirname(root));
|
|
2137
2274
|
const evidenceFile = path.relative(projectRoot, selectedWorkEvidence.assignmentFile);
|
|
2275
|
+
const pullWorkReport = path.join(dir, `${slug}--pull-work.md`);
|
|
2276
|
+
let reportIsValid = false;
|
|
2277
|
+
try {
|
|
2278
|
+
const reportStat = fs.lstatSync(pullWorkReport);
|
|
2279
|
+
reportIsValid = !reportStat.isSymbolicLink() && reportStat.isFile() && fs.readFileSync(pullWorkReport, "utf8").includes(workItem.ref);
|
|
2280
|
+
}
|
|
2281
|
+
catch { /* handled by the stable contract error below */ }
|
|
2282
|
+
if (!reportIsValid) {
|
|
2283
|
+
die(`ensure-session refused to emit selected-work evidence: expected concrete pull-work selection report ${pullWorkReport} naming ${JSON.stringify(workItem.ref)}`);
|
|
2284
|
+
}
|
|
2138
2285
|
await recordGateClaim(parseArgs([
|
|
2139
2286
|
"record-gate-claim",
|
|
2140
2287
|
dir,
|
|
@@ -2147,8 +2294,25 @@ async function ensureSession(p) {
|
|
|
2147
2294
|
file: evidenceFile,
|
|
2148
2295
|
summary: `Durable local assignment record for the selected Work Item and active actor; sha256:${assignmentDigest}.`,
|
|
2149
2296
|
}),
|
|
2297
|
+
...(selectedWorkEvidence.providerEvidenceFile ? [
|
|
2298
|
+
"--evidence-ref-json", JSON.stringify({
|
|
2299
|
+
kind: "artifact",
|
|
2300
|
+
file: path.relative(projectRoot, selectedWorkEvidence.providerEvidenceFile),
|
|
2301
|
+
summary: `Provider assignment state confirming ownership before the local runtime lease was mirrored; sha256:${providerEvidenceDigest}.`,
|
|
2302
|
+
}),
|
|
2303
|
+
] : []),
|
|
2304
|
+
"--evidence-ref-json", JSON.stringify({
|
|
2305
|
+
kind: "artifact",
|
|
2306
|
+
file: path.relative(projectRoot, pullWorkReport),
|
|
2307
|
+
summary: `Concrete pull-work selection report for ${workItem.ref}.`,
|
|
2308
|
+
}),
|
|
2150
2309
|
"--timestamp", timestamp,
|
|
2151
2310
|
]));
|
|
2311
|
+
if (selectedWorkEvidence.providerEvidenceFile && selectedWorkEvidence.providerEvidenceBytes
|
|
2312
|
+
&& !readRegularFileNoFollow(selectedWorkEvidence.providerEvidenceFile, "selected-work provider evidence").equals(selectedWorkEvidence.providerEvidenceBytes)) {
|
|
2313
|
+
die("ensure-session refused to synchronize selected-work evidence: provider evidence changed during claim recording");
|
|
2314
|
+
}
|
|
2315
|
+
await syncBuilderFlowSession({ sessionDir: dir });
|
|
2152
2316
|
});
|
|
2153
2317
|
}
|
|
2154
2318
|
}
|
|
@@ -2285,6 +2449,423 @@ export function normalizeEvidenceRefs(raw, label) {
|
|
|
2285
2449
|
return validateEvidenceRef({ ...ref }, label);
|
|
2286
2450
|
});
|
|
2287
2451
|
}
|
|
2452
|
+
function canonicalProjectRootForSession(dir) {
|
|
2453
|
+
const artifactRoot = path.dirname(dir);
|
|
2454
|
+
const kontouraiRoot = path.dirname(artifactRoot);
|
|
2455
|
+
if (path.basename(artifactRoot) !== "flow-agents" || path.basename(kontouraiRoot) !== ".kontourai")
|
|
2456
|
+
die("gate evidence requires a canonical .kontourai/flow-agents session");
|
|
2457
|
+
const projectRoot = path.dirname(kontouraiRoot);
|
|
2458
|
+
const stat = fs.lstatSync(projectRoot);
|
|
2459
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
2460
|
+
die("gate evidence project root must be a non-symlink directory");
|
|
2461
|
+
return projectRoot;
|
|
2462
|
+
}
|
|
2463
|
+
function pathIsWithinRoot(candidate, root) {
|
|
2464
|
+
const relative = path.relative(root, candidate);
|
|
2465
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
2466
|
+
}
|
|
2467
|
+
function validateLocalEvidenceFile(projectRoot, raw, label) {
|
|
2468
|
+
const candidate = path.resolve(projectRoot, raw);
|
|
2469
|
+
if (!pathIsWithinRoot(candidate, projectRoot))
|
|
2470
|
+
die(`${label} must remain inside the canonical project root`);
|
|
2471
|
+
let stat;
|
|
2472
|
+
try {
|
|
2473
|
+
stat = fs.lstatSync(candidate);
|
|
2474
|
+
}
|
|
2475
|
+
catch {
|
|
2476
|
+
die(`${label} does not exist`);
|
|
2477
|
+
}
|
|
2478
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2479
|
+
die(`${label} must be a non-symlink regular file`);
|
|
2480
|
+
if (!pathIsWithinRoot(fs.realpathSync(candidate), fs.realpathSync(projectRoot)))
|
|
2481
|
+
die(`${label} escapes the canonical project root`);
|
|
2482
|
+
return path.relative(projectRoot, candidate);
|
|
2483
|
+
}
|
|
2484
|
+
function globMatches(pattern, relative) {
|
|
2485
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**", "::GLOBSTAR::").replaceAll("*", "[^/]*").replaceAll("::GLOBSTAR::", ".*");
|
|
2486
|
+
return new RegExp(`^${escaped}$`).test(relative);
|
|
2487
|
+
}
|
|
2488
|
+
function expectedGateProducer(flowId, stepId, expectationId) {
|
|
2489
|
+
const manifest = loadJson(path.join(flowAgentsPackageRoot(), "kits", "builder", "kit.json"));
|
|
2490
|
+
const actions = Array.isArray(manifest.flow_step_actions) ? manifest.flow_step_actions : [];
|
|
2491
|
+
const action = actions.find((candidate) => candidate.flow_id === flowId && candidate.step_id === stepId);
|
|
2492
|
+
if (!action)
|
|
2493
|
+
die(`record-gate-claim cannot derive a producer for unknown Flow step ${flowId}/${stepId}`);
|
|
2494
|
+
const skills = Array.isArray(action.skills) ? action.skills.filter((value) => typeof value === "string") : [];
|
|
2495
|
+
const roles = Array.isArray(manifest.skill_roles) ? manifest.skill_roles : [];
|
|
2496
|
+
const owners = roles.filter((role) => typeof role.skill_id === "string"
|
|
2497
|
+
&& Array.isArray(role.step_ids) && role.step_ids.includes(stepId)
|
|
2498
|
+
&& Array.isArray(role.expectation_ids) && role.expectation_ids.includes(expectationId)
|
|
2499
|
+
&& skills.includes(role.skill_id.replace(/^builder\./, "")));
|
|
2500
|
+
if (owners.length === 0) {
|
|
2501
|
+
const operations = Array.isArray(action.operations) ? action.operations.filter((value) => typeof value === "string") : [];
|
|
2502
|
+
const operationExpectations = Array.isArray(action.expectation_ids) ? action.expectation_ids : [];
|
|
2503
|
+
if (operations.length === 1 && operationExpectations.includes(expectationId)) {
|
|
2504
|
+
const artifacts = Array.isArray(action.artifacts) ? action.artifacts.filter((value) => typeof value === "string") : [];
|
|
2505
|
+
return {
|
|
2506
|
+
id: `operation:${operations[0]}`,
|
|
2507
|
+
artifactPatterns: artifacts.filter((value) => !value.includes("#")),
|
|
2508
|
+
selfProducedTrustSlices: artifacts.filter((value) => value.startsWith("trust.bundle#")).map((value) => value.slice("trust.bundle#".length)),
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
if (owners.length !== 1)
|
|
2513
|
+
die(`record-gate-claim cannot derive exactly one producer for ${flowId}/${stepId}/${expectationId}`);
|
|
2514
|
+
const owner = owners[0];
|
|
2515
|
+
const artifacts = (Array.isArray(owner.artifacts) ? owner.artifacts : []).filter((value) => typeof value === "string" && value !== "ephemeral decision record");
|
|
2516
|
+
return {
|
|
2517
|
+
id: owner.skill_id,
|
|
2518
|
+
artifactPatterns: artifacts.filter((value) => !value.includes("#")),
|
|
2519
|
+
selfProducedTrustSlices: artifacts.filter((value) => value.startsWith("trust.bundle#")).map((value) => value.slice("trust.bundle#".length)),
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function validateReviewableGateEvidence(dir, slug, refs, producer, label) {
|
|
2523
|
+
if (refs.length === 0)
|
|
2524
|
+
die(`${label} requires at least one reviewable --evidence-ref-json`);
|
|
2525
|
+
const projectRoot = canonicalProjectRootForSession(dir);
|
|
2526
|
+
let declaredProducerArtifactFound = producer.artifactPatterns.length === 0;
|
|
2527
|
+
let localOrCommandEvidenceFound = false;
|
|
2528
|
+
for (const [index, ref] of refs.entries()) {
|
|
2529
|
+
if (ref.kind === "command" && hasNonEmptyString(ref.excerpt))
|
|
2530
|
+
localOrCommandEvidenceFound = true;
|
|
2531
|
+
if (typeof ref.file !== "string")
|
|
2532
|
+
continue;
|
|
2533
|
+
const relative = validateLocalEvidenceFile(projectRoot, ref.file, `${label} evidence ref ${index}`);
|
|
2534
|
+
ref.file = relative;
|
|
2535
|
+
localOrCommandEvidenceFound = true;
|
|
2536
|
+
if (ref.kind === "artifact" && producer.artifactPatterns.length > 0) {
|
|
2537
|
+
const patterns = producer.artifactPatterns.map((pattern) => {
|
|
2538
|
+
const expanded = pattern.replaceAll("<slug>", slug);
|
|
2539
|
+
return expanded.startsWith(".kontourai/") ? expanded : `.kontourai/flow-agents/${slug}/${expanded}`;
|
|
2540
|
+
});
|
|
2541
|
+
if (patterns.some((pattern) => globMatches(pattern, relative)))
|
|
2542
|
+
declaredProducerArtifactFound = true;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (!declaredProducerArtifactFound)
|
|
2546
|
+
die(`${label} requires a declared durable artifact from producer ${producer.id}`);
|
|
2547
|
+
if (producer.selfProducedTrustSlices.length > 0 && !localOrCommandEvidenceFound) {
|
|
2548
|
+
die(`${label} is produced as trust.bundle fragment(s) ${producer.selfProducedTrustSlices.join(", ")} and requires local artifact or command evidence; external-only references cannot prove a passing claim`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function commandFromEvidenceRef(ref) {
|
|
2552
|
+
return typeof ref.excerpt === "string" ? ref.excerpt.trim() : (typeof ref.url === "string" ? ref.url.trim() : "");
|
|
2553
|
+
}
|
|
2554
|
+
function hasTestIntent(name) {
|
|
2555
|
+
return /(?:^|[-_.\/])(test|tests|check|checks|verify|verification|spec|specs|eval|evals)(?:$|[-_.\/])/i.test(name) || /(?:test|check|verify|spec|eval)/i.test(path.basename(name));
|
|
2556
|
+
}
|
|
2557
|
+
function resolvesExplicitTestTarget(projectRoot, token) {
|
|
2558
|
+
if (!hasTestIntent(token))
|
|
2559
|
+
return false;
|
|
2560
|
+
try {
|
|
2561
|
+
if (/[*?\[]/.test(token))
|
|
2562
|
+
return fs.globSync(token, { cwd: projectRoot }).length > 0;
|
|
2563
|
+
const target = path.resolve(projectRoot, token);
|
|
2564
|
+
return pathIsWithinRoot(target, projectRoot) && fs.lstatSync(target).isFile();
|
|
2565
|
+
}
|
|
2566
|
+
catch {
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function staticTestUnits(file, executable) {
|
|
2571
|
+
try {
|
|
2572
|
+
const content = fs.readFileSync(file, "utf8").slice(0, 256 * 1024);
|
|
2573
|
+
const hashComments = !["cargo", "go"].includes(executable);
|
|
2574
|
+
const active = content.split("\n")
|
|
2575
|
+
.map((line) => hashComments ? line.replace(/\s+#.*$/, "") : line.replace(/\s+\/\/.*$/, ""))
|
|
2576
|
+
.filter((line) => hashComments ? !line.trimStart().startsWith("#") : !line.trimStart().startsWith("//"))
|
|
2577
|
+
.join("\n");
|
|
2578
|
+
if (["bash", "sh", "zsh"].includes(executable)
|
|
2579
|
+
&& !/(?:^|\n)\s*set\s+(?:-[^\n\s]*e[^\n\s]*|-o\s+errexit)(?:\s|$)/.test(active))
|
|
2580
|
+
return 0;
|
|
2581
|
+
const assertions = active.match(/(?:\b(?:test|it)\s*\(|\b(?:assert|expect)\s*\(|^\s*(?:async\s+)?def\s+test_|^\s*(?:public\s+)?function\s+test|^\s*it\s+["']|^\s*func\s+(?:Test|Fuzz|Example)\w*\s*\(|#\[(?:\w+::)?test\]|^(?:if\s+! ?)?(?:test\s|\[\s|\[\[\s|(?:grep|rg|diff|cmp)\s))/gm) ?? [];
|
|
2582
|
+
return assertions.length;
|
|
2583
|
+
}
|
|
2584
|
+
catch {
|
|
2585
|
+
return 0;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Produce evidence from the locally executed command and statically reviewable
|
|
2590
|
+
* test units. Runner stdout is deliberately excluded: any executable can print
|
|
2591
|
+
* a Vitest/Jest-looking success summary, but it cannot turn a non-test script
|
|
2592
|
+
* into a supported test workflow or supply this locally-created proof.
|
|
2593
|
+
*/
|
|
2594
|
+
export function testExecutionProof(command, projectRoot, seenScripts = new Set(), packageScriptBody = false) {
|
|
2595
|
+
const normalized = command.trim().replace(/\s+/g, " ");
|
|
2596
|
+
if (!normalized || /[`$()]/.test(normalized))
|
|
2597
|
+
return null;
|
|
2598
|
+
if (/[;&|]/.test(normalized)) {
|
|
2599
|
+
if (!packageScriptBody || normalized.includes("||") || /[;|]/.test(normalized))
|
|
2600
|
+
return null;
|
|
2601
|
+
const segments = normalized.split(/\s*&&\s*/).filter(Boolean);
|
|
2602
|
+
return segments.length > 1 ? segments.map((segment) => testExecutionProof(segment, projectRoot, new Set(seenScripts), false)).find(Boolean) ?? null : null;
|
|
2603
|
+
}
|
|
2604
|
+
if (/^(?:true|:|\/usr\/bin\/true)$/i.test(normalized))
|
|
2605
|
+
return null;
|
|
2606
|
+
if (/^(?:echo|printf)(?:\s|$)/i.test(normalized))
|
|
2607
|
+
return null;
|
|
2608
|
+
if (/(?:^|\s)(?:--version|-v|--help|-h)(?:\s|$)/.test(normalized))
|
|
2609
|
+
return null;
|
|
2610
|
+
const tokens = normalized.split(" ").filter(Boolean);
|
|
2611
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0] ?? ""))
|
|
2612
|
+
tokens.shift();
|
|
2613
|
+
const executable = tokens[0] ?? "";
|
|
2614
|
+
const executableName = path.basename(executable);
|
|
2615
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(executableName)) {
|
|
2616
|
+
// `bun test` is Bun's test runner; package scripts use `bun run <script>`.
|
|
2617
|
+
if (executableName === "bun" && tokens[1] === "test") {
|
|
2618
|
+
const target = tokens.slice(2).find((token) => !token.startsWith("-") && resolvesExplicitTestTarget(projectRoot, token));
|
|
2619
|
+
const units = target ? staticTestUnits(path.resolve(projectRoot, target), "bun") : 0;
|
|
2620
|
+
return units > 0 ? { kind: "local-process-exit", runner: "bun test", static_test_units: units } : null;
|
|
2621
|
+
}
|
|
2622
|
+
const script = tokens[1] === "run" || tokens[1] === "run-script" ? tokens[2] : tokens[1];
|
|
2623
|
+
if (!script || !hasTestIntent(script) || seenScripts.has(script))
|
|
2624
|
+
return null;
|
|
2625
|
+
try {
|
|
2626
|
+
const pkg = loadJson(path.join(projectRoot, "package.json"));
|
|
2627
|
+
const scriptCommand = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts[script] : undefined;
|
|
2628
|
+
if (typeof scriptCommand !== "string")
|
|
2629
|
+
return null;
|
|
2630
|
+
seenScripts.add(script);
|
|
2631
|
+
return testExecutionProof(scriptCommand, projectRoot, seenScripts, true);
|
|
2632
|
+
}
|
|
2633
|
+
catch {
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (["vitest", "jest", "mocha", "ava", "pytest", "rspec", "phpunit"].includes(executableName)
|
|
2638
|
+
&& !tokens.some((token) => /pass.?with.?no.?tests/i.test(token))) {
|
|
2639
|
+
if (executable !== executableName)
|
|
2640
|
+
return null;
|
|
2641
|
+
const targets = tokens.slice(1)
|
|
2642
|
+
.filter((token) => !token.startsWith("-") && resolvesExplicitTestTarget(projectRoot, token))
|
|
2643
|
+
.flatMap((token) => /[*?\[]/.test(token) ? fs.globSync(token, { cwd: projectRoot }) : [token]);
|
|
2644
|
+
const units = targets.reduce((total, target) => total + staticTestUnits(path.resolve(projectRoot, target), executableName), 0);
|
|
2645
|
+
return units > 0 ? { kind: "local-process-exit", runner: executableName, static_test_units: units } : null;
|
|
2646
|
+
}
|
|
2647
|
+
if (executableName === "cargo" && tokens[1] === "test") {
|
|
2648
|
+
const files = [...fs.globSync("tests/**/*.rs", { cwd: projectRoot }), ...fs.globSync("src/**/*.rs", { cwd: projectRoot })];
|
|
2649
|
+
const units = [...new Set(files)].reduce((total, file) => total + staticTestUnits(path.resolve(projectRoot, file), "cargo"), 0);
|
|
2650
|
+
return units > 0 ? { kind: "local-process-exit", runner: "cargo test", static_test_units: units } : null;
|
|
2651
|
+
}
|
|
2652
|
+
if (executableName === "go" && tokens[1] === "test") {
|
|
2653
|
+
const files = fs.globSync("**/*_test.go", { cwd: projectRoot, exclude: ["vendor/**", ".git/**"] });
|
|
2654
|
+
const units = files.reduce((total, file) => total + staticTestUnits(path.resolve(projectRoot, file), "go"), 0);
|
|
2655
|
+
return units > 0 ? { kind: "local-process-exit", runner: "go test", static_test_units: units } : null;
|
|
2656
|
+
}
|
|
2657
|
+
if (executableName === "npx" && tokens[1] && ["vitest", "jest", "mocha", "ava", "pytest"].includes(path.basename(tokens[1]))) {
|
|
2658
|
+
const runner = path.basename(tokens[1]);
|
|
2659
|
+
const targets = tokens.slice(2)
|
|
2660
|
+
.filter((token) => !token.startsWith("-") && resolvesExplicitTestTarget(projectRoot, token))
|
|
2661
|
+
.flatMap((token) => /[*?\[]/.test(token) ? fs.globSync(token, { cwd: projectRoot }) : [token]);
|
|
2662
|
+
const units = targets.reduce((total, target) => total + staticTestUnits(path.resolve(projectRoot, target), runner), 0);
|
|
2663
|
+
return units > 0 ? { kind: "local-process-exit", runner: `npx ${runner}`, static_test_units: units } : null;
|
|
2664
|
+
}
|
|
2665
|
+
if (executableName === "node" && tokens.includes("--test")) {
|
|
2666
|
+
const testFlag = tokens.indexOf("--test");
|
|
2667
|
+
const targets = tokens.slice(testFlag + 1)
|
|
2668
|
+
.filter((token) => !token.startsWith("-") && resolvesExplicitTestTarget(projectRoot, token))
|
|
2669
|
+
.flatMap((token) => /[*?\[]/.test(token) ? fs.globSync(token, { cwd: projectRoot }) : [token]);
|
|
2670
|
+
const units = targets.reduce((total, target) => total + staticTestUnits(path.resolve(projectRoot, target), executableName), 0);
|
|
2671
|
+
return units > 0 ? { kind: "local-process-exit", runner: "node --test", static_test_units: units } : null;
|
|
2672
|
+
}
|
|
2673
|
+
const scriptPath = ["bash", "sh", "zsh", "tsx", "ts-node"].includes(executableName) ? tokens[1] : executable;
|
|
2674
|
+
if (!scriptPath || ["-c", "-lc", "-e"].includes(scriptPath) || !hasTestIntent(scriptPath))
|
|
2675
|
+
return null;
|
|
2676
|
+
try {
|
|
2677
|
+
const resolved = path.resolve(projectRoot, scriptPath);
|
|
2678
|
+
const stat = fs.lstatSync(resolved);
|
|
2679
|
+
if (stat.isSymbolicLink() || !stat.isFile() || !pathIsWithinRoot(fs.realpathSync(resolved), fs.realpathSync(projectRoot)))
|
|
2680
|
+
return null;
|
|
2681
|
+
const units = staticTestUnits(resolved, executableName);
|
|
2682
|
+
return units > 0 ? { kind: "local-process-exit", runner: executableName, static_test_units: units } : null;
|
|
2683
|
+
}
|
|
2684
|
+
catch {
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
/** Validate test-evidence command shape without executing it. */
|
|
2689
|
+
export function isMeaningfulTestCommand(command, projectRoot, seenScripts = new Set(), packageScriptBody = false) {
|
|
2690
|
+
return testExecutionProof(command, projectRoot, seenScripts, packageScriptBody) !== null;
|
|
2691
|
+
}
|
|
2692
|
+
export function observedExecutedTestCount(output) {
|
|
2693
|
+
const counts = [];
|
|
2694
|
+
for (const pattern of [
|
|
2695
|
+
/^\s*(?:#|ℹ)\s*tests\s+(\d+)\s*$/gim,
|
|
2696
|
+
/^\s*1\.\.(\d+)(?:\s|$)/gim,
|
|
2697
|
+
/\b(\d+)\s+passed\b/gim,
|
|
2698
|
+
]) {
|
|
2699
|
+
for (const match of output.matchAll(pattern))
|
|
2700
|
+
counts.push(Number(match[1]));
|
|
2701
|
+
}
|
|
2702
|
+
const explicitPasses = output.match(/^\s*(?:---\s+PASS:|ok\s+\d+\s+-)/gm)?.length ?? 0;
|
|
2703
|
+
if (explicitPasses > 0)
|
|
2704
|
+
counts.push(explicitPasses);
|
|
2705
|
+
const goPackages = output.match(/^ok\s+\S+(?:\s|$)/gm)?.length ?? 0;
|
|
2706
|
+
if (goPackages > 0)
|
|
2707
|
+
counts.push(goPackages);
|
|
2708
|
+
return Math.max(0, ...counts.filter((count) => Number.isSafeInteger(count) && count > 0));
|
|
2709
|
+
}
|
|
2710
|
+
export function inferExecutedTestCount(command, projectRoot, output, seenScripts = new Set()) {
|
|
2711
|
+
const proof = testExecutionProof(command, projectRoot, seenScripts);
|
|
2712
|
+
if (!proof)
|
|
2713
|
+
return 0;
|
|
2714
|
+
const observed = observedExecutedTestCount(output);
|
|
2715
|
+
return observed > 0 ? Math.min(proof.static_test_units, observed) : 0;
|
|
2716
|
+
}
|
|
2717
|
+
async function normalizeObservedCommands(commands, projectRoot, requireTestIntent, expectedStatus) {
|
|
2718
|
+
if (commands.length === 0)
|
|
2719
|
+
die("record-gate-claim requires at least one --command for observed command evidence");
|
|
2720
|
+
if (new Set(commands).size !== commands.length)
|
|
2721
|
+
die("record-gate-claim --command values must be unique");
|
|
2722
|
+
for (const command of commands) {
|
|
2723
|
+
const { isRunnableCommandText } = loadRunnableCommandHelper();
|
|
2724
|
+
if (!isRunnableCommandText(command))
|
|
2725
|
+
die(`record-gate-claim --command "${command}" is not a runnable shell command — prose belongs in --summary, which is never executed.`);
|
|
2726
|
+
if (requireTestIntent && !isMeaningfulTestCommand(command, projectRoot))
|
|
2727
|
+
die("record-gate-claim tests-evidence command must resolve through a non-vacuous package script or a known test/check/verify/eval runner or project-local test path; shell wrappers, no-ops, version/help commands, and arbitrary node -e commands are not evidence");
|
|
2728
|
+
}
|
|
2729
|
+
// Passing test evidence is always executed exactly once by this canonical
|
|
2730
|
+
// writer. Caller-supplied observations remain available for non-test
|
|
2731
|
+
// attestations but can never stand in for locally observed test execution.
|
|
2732
|
+
const observed = await Promise.all(commands.map(async (command) => {
|
|
2733
|
+
const result = await runObservedCommand(command, projectRoot);
|
|
2734
|
+
const proof = requireTestIntent ? testExecutionProof(command, projectRoot) : null;
|
|
2735
|
+
return { command, exit_code: result.exit_code, output_sha256: result.output_sha256, ...(proof ? { test_count: inferExecutedTestCount(command, projectRoot, result.output), execution_proof: proof } : {}) };
|
|
2736
|
+
}));
|
|
2737
|
+
if (observed.length !== commands.length)
|
|
2738
|
+
die("record-gate-claim requires exactly one --observed-command-json for every --command");
|
|
2739
|
+
const byCommand = new Map();
|
|
2740
|
+
for (const entry of observed) {
|
|
2741
|
+
if (typeof entry.command !== "string" || typeof entry.exit_code !== "number" || !Number.isInteger(entry.exit_code) || typeof entry.output_sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(entry.output_sha256))
|
|
2742
|
+
die("--observed-command-json must contain command, integer exit_code, and sha256 output_sha256");
|
|
2743
|
+
if (!commands.includes(entry.command))
|
|
2744
|
+
die("--observed-command-json command must exactly match one supplied --command");
|
|
2745
|
+
if (expectedStatus === "pass" && entry.exit_code !== 0)
|
|
2746
|
+
die(`record-gate-claim passing evidence command failed (exit ${entry.exit_code}): ${entry.command}`);
|
|
2747
|
+
if (expectedStatus === "fail" && entry.exit_code === 0)
|
|
2748
|
+
die(`record-gate-claim failing evidence command unexpectedly passed: ${entry.command}`);
|
|
2749
|
+
if (requireTestIntent && (!Number.isSafeInteger(entry.test_count) || Number(entry.test_count) <= 0 || !entry.execution_proof || entry.execution_proof.kind !== "local-process-exit"))
|
|
2750
|
+
die(`record-gate-claim passing tests-evidence command did not produce a local execution proof: ${entry.command}`);
|
|
2751
|
+
if (byCommand.has(entry.command))
|
|
2752
|
+
die("--observed-command-json command values must be unique");
|
|
2753
|
+
byCommand.set(entry.command, entry);
|
|
2754
|
+
}
|
|
2755
|
+
if (commands.some((command) => !byCommand.has(command)))
|
|
2756
|
+
die("every --command requires a corresponding --observed-command-json");
|
|
2757
|
+
return commands.map((command) => byCommand.get(command));
|
|
2758
|
+
}
|
|
2759
|
+
function requireObservedCommandRefs(refs, observedCommands, label, requireAll = false) {
|
|
2760
|
+
const commandRefs = refs.filter((ref) => ref.kind === "command");
|
|
2761
|
+
if (commandRefs.length === 0)
|
|
2762
|
+
die(`${label} requires a command evidence ref matching a successful observed command`);
|
|
2763
|
+
for (const ref of commandRefs) {
|
|
2764
|
+
if (!observedCommands.has(commandFromEvidenceRef(ref)))
|
|
2765
|
+
die(`${label} command evidence ref must exactly match a successful --observed-command-json command`);
|
|
2766
|
+
}
|
|
2767
|
+
if (requireAll) {
|
|
2768
|
+
const referenced = new Set(commandRefs.map(commandFromEvidenceRef));
|
|
2769
|
+
if ([...observedCommands].some((command) => !referenced.has(command)))
|
|
2770
|
+
die(`${label} requires a top-level command evidence ref for every successful observed command`);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function completePassingCriteria(existing, raw, observedCommands) {
|
|
2774
|
+
if (raw.length === 0)
|
|
2775
|
+
die("record-gate-claim requires --criterion-json for a passing tests-evidence claim");
|
|
2776
|
+
const incoming = raw.map((value) => parseJson(value, "--criterion-json"));
|
|
2777
|
+
const expectedById = new Map();
|
|
2778
|
+
for (const criterion of existing) {
|
|
2779
|
+
const id = String(criterion.id ?? "");
|
|
2780
|
+
if (id.length > 0)
|
|
2781
|
+
expectedById.set(id, criterion);
|
|
2782
|
+
}
|
|
2783
|
+
const expectedIds = [...expectedById.keys()];
|
|
2784
|
+
const ids = incoming.map((criterion) => typeof criterion.id === "string" ? criterion.id : "");
|
|
2785
|
+
if (new Set(ids).size !== ids.length || ids.length !== expectedIds.length || ids.some((id) => !expectedIds.includes(id))) {
|
|
2786
|
+
die(`--criterion-json must cover every declared acceptance criterion exactly once (expected: ${expectedIds.join(", ") || "none"}; received: ${ids.join(", ") || "none"})`);
|
|
2787
|
+
}
|
|
2788
|
+
return incoming.map((criterion, index) => {
|
|
2789
|
+
if (Object.keys(criterion).some((key) => !["id", "status", "evidence_refs"].includes(key)))
|
|
2790
|
+
die(`criterion ${ids[index]} may update only id, status, and evidence_refs`);
|
|
2791
|
+
if (criterion.status !== "pass")
|
|
2792
|
+
die(`criterion ${ids[index]} must have status pass for a passing tests-evidence claim`);
|
|
2793
|
+
const refs = normalizeEvidenceRefs(criterion.evidence_refs, `criterion ${ids[index]} evidence_refs`);
|
|
2794
|
+
if (refs.length === 0)
|
|
2795
|
+
die(`criterion ${ids[index]} requires reviewable evidence_refs`);
|
|
2796
|
+
requireObservedCommandRefs(refs, observedCommands, `criterion ${ids[index]}`);
|
|
2797
|
+
return { ...expectedById.get(ids[index]), status: "pass", evidence_refs: refs };
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
const critiqueStatuses = new Set(["pass", "fail", "not_verified"]);
|
|
2801
|
+
const safeCritiqueId = /^[a-z][a-z0-9_-]*$/;
|
|
2802
|
+
function normalizeCritiqueLanes(raw) {
|
|
2803
|
+
if (raw.length === 0)
|
|
2804
|
+
die("record-critique requires at least one --lane-json");
|
|
2805
|
+
const lanes = raw.map((value, index) => {
|
|
2806
|
+
const lane = parseJson(value, "--lane-json");
|
|
2807
|
+
const keys = Object.keys(lane);
|
|
2808
|
+
if (keys.some((key) => !["id", "status", "summary", "evidence_refs"].includes(key)))
|
|
2809
|
+
die(`--lane-json ${index} contains unsupported fields`);
|
|
2810
|
+
if (!safeCritiqueId.test(String(lane.id ?? "")))
|
|
2811
|
+
die(`--lane-json ${index} id must be a unique safe identifier`);
|
|
2812
|
+
if (!critiqueStatuses.has(String(lane.status ?? "")))
|
|
2813
|
+
die(`--lane-json ${index} status must be one of: pass, fail, not_verified`);
|
|
2814
|
+
if (!hasNonEmptyString(lane.summary))
|
|
2815
|
+
die(`--lane-json ${index} summary must be non-empty`);
|
|
2816
|
+
const evidenceRefs = normalizeEvidenceRefs(lane.evidence_refs, `--lane-json ${index} evidence_refs`);
|
|
2817
|
+
if (evidenceRefs.length === 0)
|
|
2818
|
+
die(`--lane-json ${index} requires structured reviewable evidence_refs`);
|
|
2819
|
+
return { id: lane.id, status: lane.status, summary: lane.summary, evidence_refs: evidenceRefs };
|
|
2820
|
+
});
|
|
2821
|
+
if (new Set(lanes.map((lane) => lane.id)).size !== lanes.length)
|
|
2822
|
+
die("--lane-json ids must be unique");
|
|
2823
|
+
return lanes;
|
|
2824
|
+
}
|
|
2825
|
+
function reviewTargetArtifacts(dir, rawPaths, label) {
|
|
2826
|
+
const projectRoot = canonicalProjectRootForSession(dir);
|
|
2827
|
+
const fallback = path.join(dir, `${taskSlugFor(dir)}--deliver.md`);
|
|
2828
|
+
const paths = rawPaths.length > 0 ? rawPaths : (fs.existsSync(fallback) ? [fallback] : []);
|
|
2829
|
+
const files = paths.map((raw, index) => validateLocalEvidenceFile(projectRoot, raw, `${label} artifact ${index}`));
|
|
2830
|
+
if (new Set(files).size !== files.length)
|
|
2831
|
+
die(`${label} artifact refs must be unique`);
|
|
2832
|
+
return files.map((file) => ({ file, sha256: createHash("sha256").update(fs.readFileSync(path.join(projectRoot, file))).digest("hex") }));
|
|
2833
|
+
}
|
|
2834
|
+
function reviewTargetArtifactsMatch(dir, reviewTarget) {
|
|
2835
|
+
try {
|
|
2836
|
+
if (!reviewTarget || typeof reviewTarget !== "object" || Array.isArray(reviewTarget))
|
|
2837
|
+
return false;
|
|
2838
|
+
const artifacts = reviewTarget.artifacts;
|
|
2839
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0)
|
|
2840
|
+
return false;
|
|
2841
|
+
const projectRoot = canonicalProjectRootForSession(dir);
|
|
2842
|
+
const files = new Set();
|
|
2843
|
+
return artifacts.every((artifact) => {
|
|
2844
|
+
if (!artifact || typeof artifact !== "object")
|
|
2845
|
+
return false;
|
|
2846
|
+
const file = artifact.file;
|
|
2847
|
+
const sha256 = artifact.sha256;
|
|
2848
|
+
if (!hasNonEmptyString(file) || !/^[a-f0-9]{64}$/i.test(String(sha256)) || files.has(file))
|
|
2849
|
+
return false;
|
|
2850
|
+
files.add(file);
|
|
2851
|
+
const relative = validateLocalEvidenceFile(projectRoot, file, "critique review_target artifact");
|
|
2852
|
+
const digest = createHash("sha256").update(fs.readFileSync(path.join(projectRoot, relative))).digest("hex");
|
|
2853
|
+
return digest === sha256;
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
catch {
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function critiqueIsCleanAndCurrent(dir, critique) {
|
|
2861
|
+
if (critique.verdict !== "pass")
|
|
2862
|
+
return false;
|
|
2863
|
+
if (!Array.isArray(critique.lanes) || critique.lanes.length === 0 || critique.lanes.some((lane) => lane.status !== "pass"))
|
|
2864
|
+
return false;
|
|
2865
|
+
if (Array.isArray(critique.findings) && critique.findings.some((finding) => finding.status === "open"))
|
|
2866
|
+
return false;
|
|
2867
|
+
return reviewTargetArtifactsMatch(dir, critique.review_target);
|
|
2868
|
+
}
|
|
2288
2869
|
// #270 HIGH fix (iteration 3): the `gate-claim-` check-id prefix is RESERVED for
|
|
2289
2870
|
// record-gate-claim's own internally-constructed ids (`id: \`gate-claim-${checkId}\`` — see
|
|
2290
2871
|
// recordGateClaim below). Every OTHER writer of check ids (record-evidence --check-json,
|
|
@@ -2618,6 +3199,20 @@ function checksFromBundle(dir) {
|
|
|
2618
3199
|
const od = md && typeof md === "object" ? md.output_digest : undefined;
|
|
2619
3200
|
return od && typeof od === "object" && od.algorithm === "sha256" && typeof od.hex === "string" && od.hex.length > 0 ? od.hex : undefined;
|
|
2620
3201
|
};
|
|
3202
|
+
const observedCommandsOf = (claim) => {
|
|
3203
|
+
const md = claim.metadata;
|
|
3204
|
+
const observed = md && typeof md === "object" ? md.observed_commands : undefined;
|
|
3205
|
+
return Array.isArray(observed) ? observed.filter((entry) => typeof entry?.command === "string" && typeof entry?.exit_code === "number" && typeof entry?.output_sha256 === "string") : undefined;
|
|
3206
|
+
};
|
|
3207
|
+
const applyProducerStamp = (check, claim) => {
|
|
3208
|
+
const md = claim.metadata;
|
|
3209
|
+
if (md && typeof md.expected_producer === "string")
|
|
3210
|
+
check._producer = md.expected_producer;
|
|
3211
|
+
if (md && typeof md.recorded_by === "string")
|
|
3212
|
+
check._recorded_by = md.recorded_by;
|
|
3213
|
+
if (md && Array.isArray(md.self_produced_trust_slices))
|
|
3214
|
+
check._producer_self_produced_trust_slices = md.self_produced_trust_slices;
|
|
3215
|
+
};
|
|
2621
3216
|
// #270(a)/(c): read side of the gate_claim stamp (write side: buildTrustBundle's
|
|
2622
3217
|
// claimMetadata.gate_claim, above). Restoring expectation_id/claim_type/subject_type/step_id
|
|
2623
3218
|
// is what lets a REBUILD (record-evidence/record-critique/record-learning, after the recorded
|
|
@@ -2711,6 +3306,10 @@ function checksFromBundle(dir) {
|
|
|
2711
3306
|
const outputSha256 = outputSha256Of(claim);
|
|
2712
3307
|
if (outputSha256)
|
|
2713
3308
|
check._output_sha256 = outputSha256;
|
|
3309
|
+
const observedCommands = observedCommandsOf(claim);
|
|
3310
|
+
if (observedCommands)
|
|
3311
|
+
check._observed_commands = observedCommands;
|
|
3312
|
+
applyProducerStamp(check, claim);
|
|
2714
3313
|
applyGateClaimStamp(check, claim);
|
|
2715
3314
|
applyGateClaimShapeUnstamped(check, claim);
|
|
2716
3315
|
checks.push(check);
|
|
@@ -2733,6 +3332,10 @@ function checksFromBundle(dir) {
|
|
|
2733
3332
|
const outputSha256 = outputSha256Of(claim);
|
|
2734
3333
|
if (outputSha256)
|
|
2735
3334
|
check._output_sha256 = outputSha256;
|
|
3335
|
+
const observedCommands = observedCommandsOf(claim);
|
|
3336
|
+
if (observedCommands)
|
|
3337
|
+
check._observed_commands = observedCommands;
|
|
3338
|
+
applyProducerStamp(check, claim);
|
|
2736
3339
|
applyGateClaimStamp(check, claim);
|
|
2737
3340
|
applyGateClaimShapeUnstamped(check, claim);
|
|
2738
3341
|
checks.push(check);
|
|
@@ -2767,9 +3370,18 @@ function existingCheckStampMap(checks) {
|
|
|
2767
3370
|
*/
|
|
2768
3371
|
function readBundleState(dir) {
|
|
2769
3372
|
const acceptance = loadJson(path.join(dir, "acceptance.json"));
|
|
3373
|
+
const bundledCriteria = criteriaFromBundle(dir);
|
|
3374
|
+
const acceptedCriteria = Array.isArray(acceptance.criteria) ? acceptance.criteria : [];
|
|
3375
|
+
const contractSignature = (criteria) => JSON.stringify(criteria.map((criterion) => ({
|
|
3376
|
+
id: criterion.id ?? null,
|
|
3377
|
+
description: criterion.description ?? null,
|
|
3378
|
+
})));
|
|
3379
|
+
const criteria = acceptedCriteria.length > 0 && contractSignature(acceptedCriteria) !== contractSignature(bundledCriteria)
|
|
3380
|
+
? acceptedCriteria
|
|
3381
|
+
: (bundledCriteria.length > 0 ? bundledCriteria : acceptedCriteria);
|
|
2770
3382
|
return {
|
|
2771
3383
|
checks: checksFromBundle(dir),
|
|
2772
|
-
criteria
|
|
3384
|
+
criteria,
|
|
2773
3385
|
critiques: critiquesFromBundle(dir),
|
|
2774
3386
|
};
|
|
2775
3387
|
}
|
|
@@ -2806,14 +3418,36 @@ function critiquesFromBundle(dir) {
|
|
|
2806
3418
|
id: String(c.subjectId || "").split("/").pop() || c.id,
|
|
2807
3419
|
verdict: c.value ?? "not_verified",
|
|
2808
3420
|
summary: c.fieldOrBehavior || "",
|
|
2809
|
-
findings: [],
|
|
3421
|
+
findings: Array.isArray(md.findings) ? md.findings : [],
|
|
3422
|
+
lanes: Array.isArray(md.lanes) ? md.lanes : [],
|
|
3423
|
+
review_target: md.review_target && typeof md.review_target === "object" && !Array.isArray(md.review_target) ? md.review_target : { artifacts: [] },
|
|
2810
3424
|
reviewer: typeof md.reviewer === "string" ? md.reviewer : "tool-code-reviewer",
|
|
2811
3425
|
reviewed_at: typeof md.reviewed_at === "string" ? md.reviewed_at : (c.updatedAt || c.createdAt || now()),
|
|
2812
|
-
artifact_refs: [],
|
|
3426
|
+
artifact_refs: Array.isArray(md.artifact_refs) ? md.artifact_refs : [],
|
|
2813
3427
|
...(typeof md.superseded_by === "string" && md.superseded_by.length > 0 ? { superseded_by: md.superseded_by } : {}),
|
|
2814
3428
|
};
|
|
2815
3429
|
});
|
|
2816
3430
|
}
|
|
3431
|
+
function criteriaFromBundle(dir) {
|
|
3432
|
+
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
3433
|
+
if (!Array.isArray(bundle.claims))
|
|
3434
|
+
return [];
|
|
3435
|
+
for (const claim of bundle.claims)
|
|
3436
|
+
requireStampedClaim(claim, dir);
|
|
3437
|
+
return bundle.claims
|
|
3438
|
+
.filter((claim) => claim && claimOrigin(claim) === "acceptance")
|
|
3439
|
+
.map((claim) => {
|
|
3440
|
+
const md = claim.metadata && typeof claim.metadata === "object" && !Array.isArray(claim.metadata) ? claim.metadata : {};
|
|
3441
|
+
const saved = md.criterion && typeof md.criterion === "object" && !Array.isArray(md.criterion) ? md.criterion : {};
|
|
3442
|
+
return {
|
|
3443
|
+
id: typeof saved.id === "string" ? saved.id : String(claim.subjectId || "").split("/").pop(),
|
|
3444
|
+
description: typeof saved.description === "string" ? saved.description : (claim.fieldOrBehavior || ""),
|
|
3445
|
+
status: typeof saved.status === "string" ? saved.status : (claim.value ?? "not_verified"),
|
|
3446
|
+
evidence_refs: Array.isArray(saved.evidence_refs) ? saved.evidence_refs : [],
|
|
3447
|
+
};
|
|
3448
|
+
})
|
|
3449
|
+
.filter((criterion) => typeof criterion.id === "string" && criterion.id.length > 0);
|
|
3450
|
+
}
|
|
2817
3451
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2818
3452
|
/**
|
|
2819
3453
|
* WS8 (ADR 0020): parse the accepted-gap waiver flags. Both --accepted-gap-reason and
|
|
@@ -2876,13 +3510,11 @@ async function recordEvidence(p) {
|
|
|
2876
3510
|
// with the same id supersedes the earlier one (same-id resupply); a new id is additive.
|
|
2877
3511
|
// (_existingState was already read above, before normalizeCheck ran, so the reserved-prefix
|
|
2878
3512
|
// exists-check sees the SAME bundle snapshot the merge below uses — no second read needed.)
|
|
2879
|
-
const _criteriaStatus = verdict === "pass" ? "pass" : verdict === "fail" ? "fail" : "not_verified";
|
|
2880
|
-
const _criteriaForBundle = _existingState.criteria.map((c) => ({ ...c, status: _criteriaStatus }));
|
|
2881
3513
|
const _mergedChecks = mergeChecksById(_existingState.checks, checks);
|
|
2882
3514
|
// #268: preserve any existing critique claims (including superseded history) instead of dropping
|
|
2883
3515
|
// them — record-evidence previously hardcoded critiques:[] here, silently erasing finding history
|
|
2884
3516
|
// whenever it ran after a critique existed.
|
|
2885
|
-
assertBundleWritten(await writeTrustBundle(dir, slug, ts, _mergedChecks,
|
|
3517
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, ts, _mergedChecks, _existingState.criteria, _existingState.critiques));
|
|
2886
3518
|
const stateStatus = verdict === "pass" ? "verified" : verdict === "fail" ? "failed" : "not_verified";
|
|
2887
3519
|
writeState(dir, slug, stateStatus, "verification", ts, "Evidence recorded.");
|
|
2888
3520
|
return 0;
|
|
@@ -3089,14 +3721,23 @@ async function recordGateClaim(p) {
|
|
|
3089
3721
|
die("--route-reason is only valid with --status fail");
|
|
3090
3722
|
if (routeReason && !/^[a-z][a-z0-9_-]*$/.test(routeReason))
|
|
3091
3723
|
die("--route-reason must be a lowercase classifier identifier");
|
|
3092
|
-
//
|
|
3093
|
-
//
|
|
3094
|
-
//
|
|
3095
|
-
// Stop-short risk) lack of a dir-scoping guard: it now resolves ITS OWN actor's current.json
|
|
3096
|
-
// view, not a different actor's more-recently-written legacy pointer.
|
|
3724
|
+
// Prefer the exact session's canonical Flow projection. Actor/global current pointers are
|
|
3725
|
+
// ambient navigation state and may legitimately point at another run or lag this run.
|
|
3726
|
+
// Legacy sessions without flow_run retain the per-actor current-pointer fallback.
|
|
3097
3727
|
const flowAgentsDir = path.dirname(dir);
|
|
3098
3728
|
const gateClaimActorKey = resolveReadActorKey(p);
|
|
3099
|
-
const
|
|
3729
|
+
const sidecarState = loadJson(path.join(dir, "state.json"));
|
|
3730
|
+
const projectedRun = sidecarState.flow_run && typeof sidecarState.flow_run === "object" && !Array.isArray(sidecarState.flow_run)
|
|
3731
|
+
? sidecarState.flow_run
|
|
3732
|
+
: null;
|
|
3733
|
+
const exactFlowId = projectedRun && typeof projectedRun.definition_id === "string" ? projectedRun.definition_id : null;
|
|
3734
|
+
const exactStepId = projectedRun && typeof projectedRun.current_step === "string" ? projectedRun.current_step : null;
|
|
3735
|
+
const exactFlowContext = exactFlowId && exactStepId ? { flowId: exactFlowId, stepId: exactStepId } : undefined;
|
|
3736
|
+
const activeStep = exactFlowContext
|
|
3737
|
+
? resolveFlowStep(exactFlowContext.flowId, exactFlowContext.stepId, findRepoRootFromDir(path.dirname(flowAgentsDir)))
|
|
3738
|
+
: resolveActiveFlowStep(flowAgentsDir, gateClaimActorKey);
|
|
3739
|
+
if (exactFlowContext && !activeStep)
|
|
3740
|
+
die(`record-gate-claim cannot resolve exact session Flow step ${exactFlowContext.flowId}/${exactFlowContext.stepId}`);
|
|
3100
3741
|
if (!activeStep)
|
|
3101
3742
|
die("record-gate-claim requires an active flow step in current.json (set via ensure-session --flow-id or advance-state --flow-definition)");
|
|
3102
3743
|
if (routeReason && !activeStep.routeBackReasons.includes(routeReason)) {
|
|
@@ -3119,17 +3760,38 @@ async function recordGateClaim(p) {
|
|
|
3119
3760
|
die(`record-gate-claim: gate "${activeStep.gateId}" has ${expects.length} expects[] entries; --expectation <id> is required. Available: ${expects.map((e) => e.id).join(", ")}`);
|
|
3120
3761
|
}
|
|
3121
3762
|
const { claimType, subjectType } = targetExpectation.bundle_claim;
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3763
|
+
const gateCommands = opts(p, "command");
|
|
3764
|
+
const gateCommand = gateCommands[0] ?? "";
|
|
3765
|
+
const mustRunTests = targetExpectation.id === "tests-evidence" && statusVal === "pass";
|
|
3766
|
+
const observedCommandRaw = opts(p, "observed-command-json");
|
|
3767
|
+
if (mustRunTests && gateCommands.length === 0)
|
|
3768
|
+
die("record-gate-claim requires at least one --command for a passing tests-evidence claim");
|
|
3769
|
+
const projectRoot = gateCommands.length > 0 ? canonicalProjectRootForSession(dir) : null;
|
|
3770
|
+
const observedCommands = gateCommands.length > 0
|
|
3771
|
+
? await normalizeObservedCommands(gateCommands, projectRoot, mustRunTests, statusVal)
|
|
3772
|
+
: [];
|
|
3773
|
+
const observedCommandNames = new Set(observedCommands.map((entry) => entry.command));
|
|
3774
|
+
let outputSha256 = null;
|
|
3775
|
+
if (!mustRunTests && gateCommands.length > 1)
|
|
3776
|
+
die("record-gate-claim accepts repeatable --command only for passing tests-evidence claims");
|
|
3777
|
+
if (gateCommands.length === 0 && observedCommandRaw.length > 0)
|
|
3778
|
+
die("--observed-command-json requires --command");
|
|
3779
|
+
if (!mustRunTests && gateCommand && observedCommandRaw.length === 0) {
|
|
3780
|
+
const { isRunnableCommandText } = loadRunnableCommandHelper();
|
|
3781
|
+
if (!isRunnableCommandText(gateCommand))
|
|
3782
|
+
die(`record-gate-claim --command "${gateCommand}" is not a runnable shell command — prose belongs in --summary, which is never executed.`);
|
|
3783
|
+
}
|
|
3784
|
+
if (observedCommands.length > 0)
|
|
3785
|
+
outputSha256 = observedCommands[0].output_sha256;
|
|
3786
|
+
// Build a gate-targeted check that matchExpectsEntry maps to the declared claim tuple.
|
|
3787
|
+
// Command-backed checks retain their executable evidence classification.
|
|
3126
3788
|
const checkId = expectationId || targetExpectation.id;
|
|
3127
3789
|
// Build a minimal "external" check. Include _gate_claim_expectation_id so that
|
|
3128
3790
|
// matchExpectsEntry can do an exact lookup for multi-expects[] gates (ADR 0016 P-d Increment 2).
|
|
3129
3791
|
// normalizeCheck preserves extra underscore-prefixed fields without stripping them.
|
|
3130
3792
|
const check = {
|
|
3131
3793
|
id: `gate-claim-${checkId}`,
|
|
3132
|
-
kind: "external",
|
|
3794
|
+
kind: gateCommands.length > 0 ? "command" : "external",
|
|
3133
3795
|
status: statusVal,
|
|
3134
3796
|
summary,
|
|
3135
3797
|
_gate_claim_expectation_id: targetExpectation.id,
|
|
@@ -3137,23 +3799,31 @@ async function recordGateClaim(p) {
|
|
|
3137
3799
|
};
|
|
3138
3800
|
// Include structured evidence refs if provided
|
|
3139
3801
|
const evidenceRefs = opts(p, "evidence-ref-json").map((v) => validateEvidenceRef(parseJson(v, "--evidence-ref-json"), "--evidence-ref-json"));
|
|
3802
|
+
const producer = expectedGateProducer(exactFlowContext?.flowId ?? activeStep.flowId, activeStep.stepId, targetExpectation.id);
|
|
3803
|
+
if (statusVal === "pass")
|
|
3804
|
+
validateReviewableGateEvidence(dir, slug, evidenceRefs, producer, `gate claim ${targetExpectation.id}`);
|
|
3805
|
+
if (mustRunTests)
|
|
3806
|
+
requireObservedCommandRefs(evidenceRefs, observedCommandNames, "a passing tests-evidence claim", true);
|
|
3140
3807
|
if (evidenceRefs.length > 0) {
|
|
3141
3808
|
check.artifact_refs = evidenceRefs;
|
|
3142
3809
|
}
|
|
3810
|
+
check._producer = producer.id;
|
|
3811
|
+
check._recorded_by = gateClaimActorKey;
|
|
3812
|
+
if (producer.selfProducedTrustSlices.length > 0)
|
|
3813
|
+
check._producer_self_produced_trust_slices = producer.selfProducedTrustSlices;
|
|
3143
3814
|
// #270(b)/#412: --command gives a gate claim a real, runnable execution.label distinct from
|
|
3144
3815
|
// the human --summary prose, so stop-goal-fit.js's bundleClaimedPassCommandChecks section (B)
|
|
3145
3816
|
// finds a real command to re-run instead of falling back to fieldOrBehavior (the --summary
|
|
3146
3817
|
// prose itself), which it would otherwise execute as a shell command under
|
|
3147
3818
|
// FLOW_AGENTS_GOAL_FIT_RECHECK. Validated at record time with the same isRunnableCommandText
|
|
3148
3819
|
// heuristic the Stop-hook backstop uses (single-sourced — see loadRunnableCommandHelper).
|
|
3149
|
-
|
|
3150
|
-
if (gateCommand) {
|
|
3151
|
-
const { isRunnableCommandText } = loadRunnableCommandHelper();
|
|
3152
|
-
if (!isRunnableCommandText(gateCommand))
|
|
3153
|
-
die(`record-gate-claim --command "${gateCommand}" is not a runnable shell command — prose belongs in --summary, which is never executed.`);
|
|
3820
|
+
if (gateCommand)
|
|
3154
3821
|
check.command = gateCommand;
|
|
3155
|
-
|
|
3822
|
+
if (observedCommands.length > 0)
|
|
3823
|
+
check._observed_commands = observedCommands;
|
|
3156
3824
|
const checkNormalized = normalizeCheck(check, /* allowGateClaimPrefix */ true);
|
|
3825
|
+
if (outputSha256)
|
|
3826
|
+
checkNormalized._output_sha256 = outputSha256;
|
|
3157
3827
|
// WS8 (ADR 0020): honor the accepted-gap waiver flags for a gate claim too.
|
|
3158
3828
|
const gateWaiver = parseWaiver(p, ts);
|
|
3159
3829
|
if (gateWaiver)
|
|
@@ -3166,8 +3836,17 @@ async function recordGateClaim(p) {
|
|
|
3166
3836
|
// SAME expectation id supersedes the earlier check for that expectation (mergeChecksById); a
|
|
3167
3837
|
// gate claim against a different expectation is additive.
|
|
3168
3838
|
const _existingState = readBundleState(dir);
|
|
3839
|
+
const criteria = mustRunTests ? completePassingCriteria(_existingState.criteria, opts(p, "criterion-json"), observedCommandNames) : _existingState.criteria;
|
|
3840
|
+
if (mustRunTests) {
|
|
3841
|
+
const liveCritiques = _existingState.critiques.filter((critique) => !critique.superseded_by);
|
|
3842
|
+
if (liveCritiques.length === 0 || liveCritiques.some((critique) => !critiqueIsCleanAndCurrent(dir, critique))) {
|
|
3843
|
+
die("a passing tests-evidence claim requires a current clean critique first");
|
|
3844
|
+
}
|
|
3845
|
+
for (const criterion of criteria)
|
|
3846
|
+
validateReviewableGateEvidence(dir, slug, criterion.evidence_refs, producer, `criterion ${criterion.id}`);
|
|
3847
|
+
}
|
|
3169
3848
|
const _mergedChecks = mergeChecksById(_existingState.checks, [checkNormalized]);
|
|
3170
|
-
assertBundleWritten(await writeTrustBundle(dir, slug, ts, _mergedChecks,
|
|
3849
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, ts, _mergedChecks, criteria, _existingState.critiques, gateClaimActorKey, exactFlowContext));
|
|
3171
3850
|
return 0;
|
|
3172
3851
|
}
|
|
3173
3852
|
/**
|
|
@@ -3354,12 +4033,46 @@ export function normalizeFinding(raw) {
|
|
|
3354
4033
|
async function recordCritique(p) {
|
|
3355
4034
|
const dir = artifactDirFrom(p.positional[0] || die("artifact directory is required"));
|
|
3356
4035
|
const slug = taskSlugFor(dir, opt(p, "task-slug"));
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
const
|
|
3361
|
-
|
|
3362
|
-
|
|
4036
|
+
const verdict = opt(p, "verdict");
|
|
4037
|
+
if (!critiqueStatuses.has(verdict))
|
|
4038
|
+
die("record-critique requires --verdict pass, fail, or not_verified");
|
|
4039
|
+
const summary = opt(p, "summary");
|
|
4040
|
+
if (!hasNonEmptyString(summary))
|
|
4041
|
+
die("record-critique requires --summary");
|
|
4042
|
+
const lanes = normalizeCritiqueLanes(opts(p, "lane-json"));
|
|
4043
|
+
const reviewArtifacts = reviewTargetArtifacts(dir, opts(p, "artifact-ref"), "record-critique review_target");
|
|
4044
|
+
if (verdict === "pass" && (lanes.some((lane) => lane.status !== "pass") || reviewArtifacts.length === 0)) {
|
|
4045
|
+
die("a passing critique requires every lane to pass and at least one local reviewed --artifact-ref");
|
|
4046
|
+
}
|
|
4047
|
+
const sidecarState = loadJson(path.join(dir, "state.json"));
|
|
4048
|
+
const projectedRun = sidecarState.flow_run && typeof sidecarState.flow_run === "object" && !Array.isArray(sidecarState.flow_run)
|
|
4049
|
+
? sidecarState.flow_run
|
|
4050
|
+
: null;
|
|
4051
|
+
const exactFlowContext = projectedRun
|
|
4052
|
+
&& typeof projectedRun.definition_id === "string"
|
|
4053
|
+
&& typeof projectedRun.current_step === "string"
|
|
4054
|
+
? { flowId: projectedRun.definition_id, stepId: projectedRun.current_step }
|
|
4055
|
+
: undefined;
|
|
4056
|
+
// trust.bundle is the sole critique source. critique.json is unsupported historical residue.
|
|
4057
|
+
const _critiqueState = readBundleState(dir);
|
|
4058
|
+
const bundleCritiques = _critiqueState.critiques;
|
|
4059
|
+
const critiqueId = opt(p, "id", "review");
|
|
4060
|
+
if (!safeCritiqueId.test(critiqueId))
|
|
4061
|
+
die("record-critique --id must be a safe identifier");
|
|
4062
|
+
const critique = {
|
|
4063
|
+
id: critiqueId,
|
|
4064
|
+
reviewer: opt(p, "reviewer", "tool-code-reviewer"),
|
|
4065
|
+
reviewed_at: opt(p, "timestamp", now()),
|
|
4066
|
+
verdict,
|
|
4067
|
+
summary,
|
|
4068
|
+
lanes,
|
|
4069
|
+
review_target: {
|
|
4070
|
+
artifacts: reviewArtifacts,
|
|
4071
|
+
workspace_snapshot: captureReviewWorkspaceSnapshot(canonicalProjectRootForSession(dir), reviewArtifacts.map((artifact) => ({ file: String(artifact.file), sha256: String(artifact.sha256) }))),
|
|
4072
|
+
},
|
|
4073
|
+
artifact_refs: reviewArtifacts.map((artifact) => artifact.file),
|
|
4074
|
+
findings: opts(p, "finding-json").map((v) => normalizeFinding(parseJson(v, "--finding-json"))),
|
|
4075
|
+
};
|
|
3363
4076
|
if (critique.verdict === "pass" && critique.findings.some((f) => f.status === "open"))
|
|
3364
4077
|
die("required critique must pass");
|
|
3365
4078
|
// #267/#282: supersede-by-critique-id. The latest write for a critique id wins for
|
|
@@ -3386,8 +4099,7 @@ async function recordCritique(p) {
|
|
|
3386
4099
|
// checksFromBundle + acceptance.json read inline — this is the exact pattern readBundleState
|
|
3387
4100
|
// already exists to share; recordLearning uses it directly (see below) and recordCritique
|
|
3388
4101
|
// previously duplicated it by hand for no reason.
|
|
3389
|
-
|
|
3390
|
-
assertBundleWritten(await writeTrustBundle(dir, slug, critique.reviewed_at, _critiqueState.checks, _critiqueState.criteria, critiques));
|
|
4102
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, critique.reviewed_at, _critiqueState.checks, _critiqueState.criteria, critiques, undefined, exactFlowContext));
|
|
3391
4103
|
return 0;
|
|
3392
4104
|
}
|
|
3393
4105
|
function frontmatter(text, key) {
|
|
@@ -3413,7 +4125,22 @@ async function importCritique(p) {
|
|
|
3413
4125
|
const title = m.groups?.title ?? "finding";
|
|
3414
4126
|
findings.push({ id: slugify(title, `finding-${findings.length + 1}`), severity: (m.groups?.severity ?? "info").toLowerCase(), status: opt(p, "finding-status", verdict === "pass" ? "fixed" : "open"), description: title, file_refs: [m.groups?.target ?? review] });
|
|
3415
4127
|
}
|
|
3416
|
-
const
|
|
4128
|
+
const importedArtifact = path.relative(canonicalProjectRootForSession(dir), review);
|
|
4129
|
+
const parsed = {
|
|
4130
|
+
...p,
|
|
4131
|
+
positional: [dir],
|
|
4132
|
+
opts: {
|
|
4133
|
+
...p.opts,
|
|
4134
|
+
id: [slugify(path.basename(review).replace(/\.md$/, ""), "review")],
|
|
4135
|
+
reviewer: ["tool-code-reviewer"],
|
|
4136
|
+
verdict: [verdict],
|
|
4137
|
+
summary: [`Imported critique from ${path.basename(review)}`],
|
|
4138
|
+
"artifact-ref": [importedArtifact],
|
|
4139
|
+
"lane-json": [JSON.stringify({ id: "imported-review", status: verdict, summary: `Imported review artifact ${path.basename(review)}.`, evidence_refs: [{ kind: "artifact", file: importedArtifact, summary: "Imported review artifact." }] })],
|
|
4140
|
+
"finding-json": findings.map((f) => JSON.stringify(f)),
|
|
4141
|
+
},
|
|
4142
|
+
flags: p.flags,
|
|
4143
|
+
};
|
|
3417
4144
|
const result = await recordCritique(parsed);
|
|
3418
4145
|
if (verdict !== "pass")
|
|
3419
4146
|
die("required critique must pass");
|
|
@@ -4763,10 +5490,7 @@ function evidenceClean(dir) {
|
|
|
4763
5490
|
});
|
|
4764
5491
|
}
|
|
4765
5492
|
function critiqueClean(dir) {
|
|
4766
|
-
//
|
|
4767
|
-
// legacy (pre-bundle-era) sessions — unrelated to origin stamping. When a trust.bundle IS
|
|
4768
|
-
// present, every claim must be stamped (requireStampedClaim); no claimType-derivation fallback
|
|
4769
|
-
// for an unstamped claim (#268/#344).
|
|
5493
|
+
// trust.bundle is the sole critique artifact. Legacy critique.json must not influence gates.
|
|
4770
5494
|
const bundle = loadJson(path.join(dir, "trust.bundle"));
|
|
4771
5495
|
if (Array.isArray(bundle.claims)) {
|
|
4772
5496
|
for (const c of bundle.claims)
|
|
@@ -4781,14 +5505,11 @@ function critiqueClean(dir) {
|
|
|
4781
5505
|
});
|
|
4782
5506
|
if (critiqueClaims.length === 0)
|
|
4783
5507
|
return false; // no critique written yet
|
|
4784
|
-
return
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
});
|
|
5508
|
+
return critiquesFromBundle(dir)
|
|
5509
|
+
.filter((critique) => !critique.superseded_by)
|
|
5510
|
+
.every((critique) => critiqueIsCleanAndCurrent(dir, critique));
|
|
4788
5511
|
}
|
|
4789
|
-
|
|
4790
|
-
const c = loadJson(path.join(dir, "critique.json"), {});
|
|
4791
|
-
return c.status === "pass" && Array.isArray(c.critiques) && c.critiques.every((x) => x.verdict !== "fail" && (!Array.isArray(x.findings) || x.findings.every((f) => f.status !== "open" && (f.file_refs === undefined || Array.isArray(f.file_refs)))));
|
|
5512
|
+
return false;
|
|
4792
5513
|
}
|
|
4793
5514
|
function assertExistingLearningValid(dir) {
|
|
4794
5515
|
const file = path.join(dir, "learning.json");
|
|
@@ -4846,8 +5567,7 @@ async function dogfoodPass(p) {
|
|
|
4846
5567
|
die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
4847
5568
|
if (!opt(p, "critique-id") && !critiqueClean(dir))
|
|
4848
5569
|
die("requires passing critique");
|
|
4849
|
-
|
|
4850
|
-
if (!critiqueClean(dir) && (fs.existsSync(path.join(dir, "trust.bundle")) || fs.existsSync(path.join(dir, "critique.json"))))
|
|
5570
|
+
if (!critiqueClean(dir) && fs.existsSync(path.join(dir, "trust.bundle")))
|
|
4851
5571
|
die(opt(p, "release-decision") ? "requires clean critique" : "requires clean critique before recording pass evidence");
|
|
4852
5572
|
}
|
|
4853
5573
|
}
|
|
@@ -5105,7 +5825,7 @@ async function gateReview(p) {
|
|
|
5105
5825
|
// Locate trust.bundle — required per SKILL.md contract
|
|
5106
5826
|
const bundlePath = path.join(dir, "trust.bundle");
|
|
5107
5827
|
if (!fs.existsSync(bundlePath)) {
|
|
5108
|
-
process.stderr.write(`[gate-review] trust.bundle absent at ${bundlePath} — NOT_VERIFIED.
|
|
5828
|
+
process.stderr.write(`[gate-review] trust.bundle absent at ${bundlePath} — NOT_VERIFIED. Record the required trust bundle before running gate review.\n`);
|
|
5109
5829
|
return 1;
|
|
5110
5830
|
}
|
|
5111
5831
|
// Load Surface (ESM, fail-open)
|
|
@@ -5396,7 +6116,7 @@ function loadLivenessReadHelper() {
|
|
|
5396
6116
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
5397
6117
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
5398
6118
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
5399
|
-
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
6119
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "archived"]);
|
|
5400
6120
|
/**
|
|
5401
6121
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
5402
6122
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|
|
@@ -5840,8 +6560,11 @@ Available claim ids:
|
|
|
5840
6560
|
return 0;
|
|
5841
6561
|
}
|
|
5842
6562
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5843
|
-
|
|
5844
|
-
|
|
6563
|
+
export function mainFromPublicWorkflow(argv) {
|
|
6564
|
+
return main(argv, PUBLIC_WORKFLOW_AUTHORITY);
|
|
6565
|
+
}
|
|
6566
|
+
export async function main(argv = process.argv.slice(2), authority) {
|
|
6567
|
+
const _rawArgv = argv;
|
|
5845
6568
|
// #380: `record-check <dir> -- <command...>` — argv after the FIRST literal `--` token is the
|
|
5846
6569
|
// command to execute verbatim (never option-parsed: a command like `npm test -- --watch`
|
|
5847
6570
|
// legitimately contains its OWN `--`, so only the record-check dispatcher's own separator, the
|
|
@@ -5882,7 +6605,7 @@ async function main() {
|
|
|
5882
6605
|
: p.command === "record-agent-event" ? explicitArtifactRoot(p) : p.command === "claim" ? (p.positional[1] ? path.resolve(p.positional[1]) : "") : p.command === "resolve-slug" ? "" : (isLivenessWhoami || isLivenessVerdict) ? "" : p.positional[0] ? artifactDirFrom(p.positional[0]) : "";
|
|
5883
6606
|
return withLock(lockRoot, ["ensure-session", "record-agent-event", "dogfood-pass"].includes(p.command), p.command, () => {
|
|
5884
6607
|
switch (p.command) {
|
|
5885
|
-
case "ensure-session": return ensureSession(p);
|
|
6608
|
+
case "ensure-session": return ensureSession(p, authority === PUBLIC_WORKFLOW_AUTHORITY);
|
|
5886
6609
|
case "current": return current(p);
|
|
5887
6610
|
case "record-agent-event": return recordAgentEvent(p);
|
|
5888
6611
|
case "init-plan": return initPlan(p);
|
|
@@ -5927,5 +6650,8 @@ catch {
|
|
|
5927
6650
|
return process.argv[1];
|
|
5928
6651
|
} })();
|
|
5929
6652
|
if (_selfRealPath === _argv1RealPath) {
|
|
6653
|
+
if (path.basename(process.argv[1] ?? "") === "flow-agents-workflow-sidecar") {
|
|
6654
|
+
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");
|
|
6655
|
+
}
|
|
5930
6656
|
main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
|
|
5931
6657
|
}
|