ai-foreman 1.7.28 → 2.0.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/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +228 -24
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +86 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +30 -0
- package/dist/buildApproval.js +68 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +32 -24
- package/dist/cli/tickets.js +40 -24
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +20 -5
- package/dist/foreman.js +57 -187
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +46 -0
- package/dist/hostGraphApplicability.js +97 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +81 -0
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -28
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +48 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +11 -7
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -229
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.js +34 -6
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +232 -394
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +3 -2
- package/dist/qaVerificationContract.js +58 -34
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +78 -31
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +781 -35
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +15 -7
package/dist/markers.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Native operation instructions. Extracted from foreman.ts so tests can import
|
|
3
3
|
* them without pulling in @clack/prompts (which requires Node >=20 for styleText).
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
export declare const
|
|
5
|
+
/** Active work protocol; legacy status parsers remain historical readers. */
|
|
6
|
+
export declare const MARKER_SPEC = "Return one work-report result through the configured native JSON schema. result.kind completed is your claim that the assigned work is finished; do not add a duplicate status field. The payload contains disposition modified or already-satisfied, truthful evidence, coverage when a verification contract is supplied (otherwise null), and graphMaintenance (null unless a meaningful indexed change occurred). Blocked work and questions use their own result alternatives. Never claim completion merely because a turn ended. Include bounded continuity when requested. The host still requires independent QA before completing a ticket.";
|
|
7
|
+
export declare const QA_MARKER_SPEC = "Return one qa-verdict result through the configured native JSON schema. A completed verdict payload is either passed with independent coverage, or failed with the complete structured failure report and coverage. Report blocked work or a question explicitly. A pass cannot contain blocking findings; preserve all failures and uncertainties. Include required continuity in the same object.";
|
package/dist/markers.js
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Native operation instructions. Extracted from foreman.ts so tests can import
|
|
3
3
|
* them without pulling in @clack/prompts (which requires Node >=20 for styleText).
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
STEP_STATUS: plan_complete | ticket="T001" summary="what you just did"
|
|
9
|
-
STEP_STATUS: needs_input | ticket="T001" question="your question for the user" choices="Option A|Option B|Option C"
|
|
10
|
-
Use "done" after finishing a ticket or step when more remain, "plan_complete" after
|
|
11
|
-
the final ticket or step, "blocked" if you cannot proceed without help, and "needs_input"
|
|
12
|
-
if you need the user to make a decision before continuing.
|
|
13
|
-
Always include the assigned ticket="<ticket-id>" in every work response marker, including needs_input, when working from a ticket queue. Never substitute another ticket.
|
|
14
|
-
After "done" or "plan_complete", foreman may run a QA pass on your work — expect a
|
|
15
|
-
follow-up instruction asking you to triple-check accuracy, tests, and ticket satisfaction.`;
|
|
16
|
-
export const QA_MARKER_SPEC = `End EVERY turn with exactly one marker line as the LAST line, nothing after it:
|
|
17
|
-
STEP_STATUS: qa_pass | summary="confirmed everything checks out"
|
|
18
|
-
RAFI_QA_FAILURE_REPORT_START
|
|
19
|
-
{"version":1,"summary":"...","checks_run":[...],"findings":[...],"observations":[...]}
|
|
20
|
-
RAFI_QA_FAILURE_REPORT_END
|
|
21
|
-
STEP_STATUS: qa_fail | issues="short plain-text synopsis"
|
|
22
|
-
STEP_STATUS: blocked | reason="why QA itself cannot proceed"
|
|
23
|
-
STEP_STATUS: needs_input | question="..." choices="..."`;
|
|
5
|
+
/** Active work protocol; legacy status parsers remain historical readers. */
|
|
6
|
+
export const MARKER_SPEC = `Return one work-report result through the configured native JSON schema. result.kind completed is your claim that the assigned work is finished; do not add a duplicate status field. The payload contains disposition modified or already-satisfied, truthful evidence, coverage when a verification contract is supplied (otherwise null), and graphMaintenance (null unless a meaningful indexed change occurred). Blocked work and questions use their own result alternatives. Never claim completion merely because a turn ended. Include bounded continuity when requested. The host still requires independent QA before completing a ticket.`;
|
|
7
|
+
export const QA_MARKER_SPEC = `Return one qa-verdict result through the configured native JSON schema. A completed verdict payload is either passed with independent coverage, or failed with the complete structured failure report and coverage. Report blocked work or a question explicitly. A pass cannot contain blocking findings; preserve all failures and uncertainties. Include required continuity in the same object.`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type HostIpc } from "rafi-spec";
|
|
2
|
+
type ProbeTerminal = Extract<HostIpc, {
|
|
3
|
+
kind: "rafi-probe-result" | "rafi-probe-spawn-error";
|
|
4
|
+
}>;
|
|
5
|
+
/** One registered helper may publish one terminal result. Identical delivery is idempotent. */
|
|
6
|
+
export declare class ProbeIpcTracker {
|
|
7
|
+
private readonly id;
|
|
8
|
+
private readonly pid;
|
|
9
|
+
private registered;
|
|
10
|
+
terminal?: ProbeTerminal;
|
|
11
|
+
constructor(id: string, pid: number | undefined);
|
|
12
|
+
accept(raw: unknown): HostIpc | "conflict" | undefined;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
package/dist/probeIpc.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseHostIpc } from "rafi-spec";
|
|
2
|
+
/** One registered helper may publish one terminal result. Identical delivery is idempotent. */
|
|
3
|
+
export class ProbeIpcTracker {
|
|
4
|
+
id;
|
|
5
|
+
pid;
|
|
6
|
+
registered = false;
|
|
7
|
+
terminal;
|
|
8
|
+
constructor(id, pid) {
|
|
9
|
+
this.id = id;
|
|
10
|
+
this.pid = pid;
|
|
11
|
+
}
|
|
12
|
+
accept(raw) {
|
|
13
|
+
const message = parseHostIpc(raw);
|
|
14
|
+
if (!message || !("id" in message) || message.id !== this.id || message.pid !== this.pid)
|
|
15
|
+
return;
|
|
16
|
+
if (message.kind === "rafi-probe-registered") {
|
|
17
|
+
if (this.registered)
|
|
18
|
+
return;
|
|
19
|
+
this.registered = true;
|
|
20
|
+
return message;
|
|
21
|
+
}
|
|
22
|
+
if (!this.registered || (message.kind !== "rafi-probe-result" && message.kind !== "rafi-probe-spawn-error"))
|
|
23
|
+
return;
|
|
24
|
+
if (this.terminal)
|
|
25
|
+
return JSON.stringify(this.terminal) === JSON.stringify(message) ? undefined : "conflict";
|
|
26
|
+
this.terminal = message;
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/qaBuildGate.d.ts
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
|
|
2
2
|
import type { QaSessionHandle } from "./qaReview.js";
|
|
3
|
+
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
3
4
|
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
4
5
|
export declare function preparationTurnPayload(turn: TurnResult): {
|
|
5
6
|
text: string;
|
|
6
7
|
formatErrors: string[];
|
|
7
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* The selected QA bundle is already injected as immutable system context for
|
|
11
|
+
* the isolated provider session. Repeat only its identity here: the exact
|
|
12
|
+
* requirements remain in the authoritative inventory, while repeating full
|
|
13
|
+
* rules and skill bodies in every operation wastes context and makes native
|
|
14
|
+
* structured submission needlessly fragile.
|
|
15
|
+
*/
|
|
16
|
+
export declare function preparationConfigurationReference(effective: ReturnType<typeof resolveEffectiveQaConfiguration>): {
|
|
17
|
+
digest: string;
|
|
18
|
+
configRoot: string;
|
|
19
|
+
roleSource: "compiled" | "library" | "fallback";
|
|
20
|
+
checklistDigest: string;
|
|
21
|
+
qaRulesDigest: string;
|
|
22
|
+
builderRulesDigest: string;
|
|
23
|
+
skills: {
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
digest: string;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
8
29
|
/** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
|
|
9
30
|
export declare function ensureBuilderContract(projectDir: string, runId: string, workId: string, workspace: string, builder: BuilderAdapter, qaFactory?: (cwd: string) => Promise<QaSessionHandle>): Promise<string>;
|
package/dist/qaBuildGate.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { operationPayload } from "./structuredOperation.js";
|
|
2
|
+
import { loadGraphConfig } from "./graph/config.js";
|
|
1
3
|
import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
|
|
2
4
|
import { createHash } from "node:crypto";
|
|
3
5
|
import { readFileSync, realpathSync } from "node:fs";
|
|
@@ -10,11 +12,12 @@ import { checkQaPrerequisites } from "./qaPrerequisites.js";
|
|
|
10
12
|
import { OperationDeadline } from "./util/deadline.js";
|
|
11
13
|
import { sendPreparationGraphTurn } from "./graph/preparation.js";
|
|
12
14
|
import { withGraphDerivedAccess } from "./graph/derived.js";
|
|
15
|
+
import { assessmentOutput, challengeOutput, depthOutput, draftOutput } from "rafi-spec";
|
|
13
16
|
import { WorkflowDb } from "./workflowDb.js";
|
|
14
17
|
import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
|
|
15
18
|
import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
|
|
16
19
|
import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
|
|
17
|
-
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract
|
|
20
|
+
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, ensureHostScopeCheck, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract } from "./qaVerificationContract.js";
|
|
18
21
|
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
19
22
|
import { prepareVerificationContract } from "./qaPreparation.js";
|
|
20
23
|
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
@@ -23,6 +26,7 @@ import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
|
|
|
23
26
|
import { renderBuildWorkContext } from "./buildWorkContext.js";
|
|
24
27
|
import { compareContractInputs } from "./qaContractFreshness.js";
|
|
25
28
|
import { readBuildRuns } from "./buildRuns.js";
|
|
29
|
+
import { normalizeGraphQaChecks } from "./hostGraphApplicability.js";
|
|
26
30
|
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
27
31
|
export function preparationTurnPayload(turn) {
|
|
28
32
|
const formatErrors = turn.continuityErrors ?? [];
|
|
@@ -31,6 +35,24 @@ export function preparationTurnPayload(turn) {
|
|
|
31
35
|
formatErrors,
|
|
32
36
|
};
|
|
33
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The selected QA bundle is already injected as immutable system context for
|
|
40
|
+
* the isolated provider session. Repeat only its identity here: the exact
|
|
41
|
+
* requirements remain in the authoritative inventory, while repeating full
|
|
42
|
+
* rules and skill bodies in every operation wastes context and makes native
|
|
43
|
+
* structured submission needlessly fragile.
|
|
44
|
+
*/
|
|
45
|
+
export function preparationConfigurationReference(effective) {
|
|
46
|
+
return {
|
|
47
|
+
digest: effective.digest,
|
|
48
|
+
configRoot: effective.configRoot,
|
|
49
|
+
roleSource: effective.roleSource,
|
|
50
|
+
checklistDigest: contractDigest("input-checklist", effective.checklist),
|
|
51
|
+
qaRulesDigest: contractDigest("qa-rules", effective.qaRules),
|
|
52
|
+
builderRulesDigest: contractDigest("builder-rules", effective.builderRules),
|
|
53
|
+
skills: effective.skills.map(skill => ({ name: skill.name, path: skill.path, digest: skill.digest })),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
34
56
|
/** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
|
|
35
57
|
export async function ensureBuilderContract(projectDir, runId, workId, workspace, builder, qaFactory) {
|
|
36
58
|
return withGraphDerivedAccess([], () => ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory));
|
|
@@ -59,6 +81,9 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
59
81
|
if (!store.eventRecord(configurationEvent))
|
|
60
82
|
store.event(runId, workId, configurationEvent, "effective-preparation-configuration", { policy, effectiveDigest: effective.digest, configRoot: effective.configRoot, roleSource: effective.roleSource, provider: builder.agent, capabilities: builder.contractCapabilities?.() ?? null, finalReviewEnabled: db.getRun(runId)?.state?.qaEnabled ?? null });
|
|
61
83
|
const inputs = [
|
|
84
|
+
{ id: "communication-contract", kind: "policy", reference: "structured-provider-output-v1", digest: contractDigest("communication-contract", {
|
|
85
|
+
protocolVersion: 1, hostValidation: "typed-invariants-v1", schemas: [draftOutput, depthOutput, assessmentOutput, challengeOutput].map(output => ({ operation: output.operation, version: output.schemaVersion, digest: output.schemaDigest })),
|
|
86
|
+
}), authority: "planner-policy", revision: "1", availability: "available" },
|
|
62
87
|
{ id: "admitted-ticket", kind: "ticket", reference: admission.assignmentId, digest: contractDigest("input-ticket", ticket), authority: "approved-scope", revision: admission.scopeRevision, availability: "available" },
|
|
63
88
|
{ id: "project-checklist", kind: "checklist", reference: "tickets.build.validation_checklist", digest: contractDigest("input-checklist", effective.checklist), authority: "project-rule", revision: "current", availability: "available" },
|
|
64
89
|
{ id: "effective-rules", kind: "rules", reference: effective.configRoot, digest: effective.digest, authority: "project-rule", revision: "current", availability: "available" },
|
|
@@ -107,6 +132,8 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
107
132
|
const qaHead = db.qaTicketHead(runId, workId);
|
|
108
133
|
if (["completed", "waived", "finalizing", "turn-intended", "turn-uncertain", "remediation-intended", "remediation-uncertain"].includes(qaHead.state))
|
|
109
134
|
throw new Error("Active or finalized QA authority requires explicit recovery/reopen before contract amendment");
|
|
135
|
+
if (!priorBudget)
|
|
136
|
+
throw new Error("Retained contract lacks its preparation budget; explicit recovery is required before amendment");
|
|
110
137
|
const budget = priorBudget;
|
|
111
138
|
if (retainedIntent?.inputDigest !== inputDigest || retainedIntent.predecessorDigest !== old.contentDigest)
|
|
112
139
|
store.beginAmendment(budget, inventory, { reason: `Canonical authorized inputs changed: ${[...comparison.changed, ...comparison.unavailable].join(", ") || "inputs reverted during amendment"}`, inputDigest, predecessorDigest: old.contentDigest });
|
|
@@ -129,7 +156,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
129
156
|
provider: builder.agent,
|
|
130
157
|
store, now: Date.now, hostFacts: `${qaHostGraphFact(projectDir)}\n${deliveryFact}\nHost-owned baseline fact: Rafi captured the complete preimplementation per-file path manifest and will perform the exact unchanged-file and scope comparison. Its digest is ${preimplementationPathManifestDigest}. Do not require the provider to receive or reproduce the raw manifest; cite the retained host-preimplementation-source observation and assess the host comparison result instead. Do not author a provider-executed check for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; Rafi performs that comparison outside the provider snapshot. Ordinary approved product paths, exports, and tests are not a material QA concern merely because the provider cannot prove whether they were present before implementation; Rafi owns that comparison. Do not reconstruct or disclose the full baseline. For current-branch additive tickets, a provider-authored or inferred request to test the branch with an already-completed sibling feature removed is not an applicable requirement when the approved scope says not to remove or reconstruct that sibling; verify the ticket's own behavior and tests against the shared baseline instead. Treat sibling-absence testing as binding only when the user explicitly approved that absence requirement. Escalate only an actual host-observed out-of-scope, destructive, protected-file, or requirement-relevant change.`,
|
|
131
158
|
normalizeDraft: (draft, response) => {
|
|
132
|
-
|
|
159
|
+
// The dispatch already retains the selected contract digest. Do not
|
|
160
|
+
// recursively embed its validator/schema in every evidence summary.
|
|
161
|
+
const { output, ...providerReceipt } = response;
|
|
162
|
+
const investigationEvidence = store.putArtifact("investigation-evidence", {
|
|
163
|
+
...providerReceipt,
|
|
164
|
+
...(output ? { selectedContract: { operation: output.operation, schemaVersion: output.schemaVersion, schemaDigest: output.schemaDigest } } : {}),
|
|
165
|
+
});
|
|
166
|
+
const normalized = allocateStableCheckIds(bindUniqueBaselineCheckIds(normalizeGraphQaChecks(ensureHostScopeCheck(draft, hostBaseline.id), loadGraphConfig(projectDir).enabled)));
|
|
133
167
|
if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
|
|
134
168
|
normalized.baseline.unshift(structuredClone(hostBaseline));
|
|
135
169
|
for (const check of normalized.checks)
|
|
@@ -142,7 +176,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
142
176
|
observation.sourceDigest = source.digest;
|
|
143
177
|
observation.observedAt = new Date().toISOString();
|
|
144
178
|
observation.phase = "preimplementation-investigation";
|
|
145
|
-
observation.evidenceDigest = store.putArtifact("baseline-observation", { observation: original, sourceDigest: source.digest, operationId: response.operationId, sessionId: response.sessionId, providerReceipt:
|
|
179
|
+
observation.evidenceDigest = store.putArtifact("baseline-observation", { observation: original, sourceDigest: source.digest, operationId: response.operationId, sessionId: response.sessionId, providerReceipt: investigationEvidence });
|
|
146
180
|
}
|
|
147
181
|
for (const evidence of normalized.preparationEvidence) {
|
|
148
182
|
evidence.operationId = response.operationId;
|
|
@@ -150,8 +184,10 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
150
184
|
delete evidence.graphReceiptRefs;
|
|
151
185
|
if (response.graphReceiptRefs?.length)
|
|
152
186
|
evidence.graphReceiptRefs = [...response.graphReceiptRefs];
|
|
153
|
-
evidence.evidenceDigest =
|
|
187
|
+
evidence.evidenceDigest = investigationEvidence;
|
|
154
188
|
for (const reference of evidence.references) {
|
|
189
|
+
if (response.workspacePaths && Object.hasOwn(response.workspacePaths, reference.path))
|
|
190
|
+
reference.path = response.workspacePaths[reference.path];
|
|
155
191
|
let path;
|
|
156
192
|
try {
|
|
157
193
|
path = realpathSync(resolve(workspace, reference.path));
|
|
@@ -182,7 +218,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
182
218
|
const compared = compareContractInputs(candidate, inputs.map(input => input.id === "effective-rules" ? { ...input, digest: current.digest } : input.id === "project-checklist" ? { ...input, digest: contractDigest("input-checklist", current.checklist) } : input.id === "preimplementation-source" ? { ...input, digest: currentSource.digest, revision: currentSource.head } : input));
|
|
183
219
|
return !compared.changed.length && !compared.unavailable.length;
|
|
184
220
|
},
|
|
185
|
-
dispatch: async (phase, instruction, operationId, remainingMs) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
|
|
221
|
+
dispatch: async (phase, instruction, operationId, remainingMs, output) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
|
|
186
222
|
const started = performance.now();
|
|
187
223
|
const remaining = () => {
|
|
188
224
|
const value = remainingMs - (performance.now() - started);
|
|
@@ -194,14 +230,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
194
230
|
if (phase === "planner") {
|
|
195
231
|
const role = await createRoleBuilder({ projectDir: workspace, configRoot: projectDir, role: "planner", label: "QA depth assessment", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false });
|
|
196
232
|
try {
|
|
197
|
-
const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan.
|
|
233
|
+
const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan. Return the requested native depth result.\n${instruction}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen effective configuration reference (authoritative requirements are in the candidate): ${JSON.stringify(preparationConfigurationReference(effective))}`, { phase, projectDir, workspace, sourceRef: before.digest, runId, workId, operationId, remainingMs: remaining(), output });
|
|
198
234
|
store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
|
|
199
235
|
if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
|
|
200
236
|
throw new Error("Product source changed during confined planning; reconcile retained dispatch");
|
|
201
237
|
const payload = preparationTurnPayload(turn);
|
|
202
|
-
if (
|
|
238
|
+
if (turn.isError || turn.failure || !role.builder.sessionId())
|
|
203
239
|
throw new Error(`planner-unavailable: ${turn.text}`);
|
|
204
|
-
return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
|
|
240
|
+
return { text: payload.text, structured: turn.structured, selectedDispatch: turn.selectedDispatch, output, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
|
|
205
241
|
}
|
|
206
242
|
finally {
|
|
207
243
|
await role.builder.close();
|
|
@@ -212,21 +248,21 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
212
248
|
try {
|
|
213
249
|
remaining();
|
|
214
250
|
const prerequisites = await checkQaPrerequisites({ snapshotPath: snapshot.path, sourceDigest: snapshot.frozenState.digest, ticket });
|
|
215
|
-
const prerequisiteDigest = db.putEvidence("qa", JSON.stringify(prerequisites));
|
|
251
|
+
const prerequisiteDigest = db.putEvidence("qa", Buffer.from(JSON.stringify(prerequisites)));
|
|
216
252
|
remaining();
|
|
217
253
|
if (qaFactory)
|
|
218
254
|
adapter = (await qaFactory(snapshot.path)).adapter;
|
|
219
255
|
else
|
|
220
256
|
adapter = (await createRoleBuilder({ projectDir: snapshot.path, configRoot: projectDir, role: "qa", agent: state?.qa?.settings?.make ?? builder.agent, preloadedSkillContent: effective.skills.map(skill => ({ name: skill.name, content: skill.content })), label: "QA preparation", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false })).builder;
|
|
221
|
-
const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration: ${JSON.stringify(effective)}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining() });
|
|
257
|
+
const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration reference (the exact QA rules and skills are already loaded as immutable system context; authoritative requirements are in the inventory): ${JSON.stringify(preparationConfigurationReference(effective))}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining(), output });
|
|
222
258
|
store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
|
|
223
259
|
const payload = preparationTurnPayload(turn);
|
|
224
260
|
payload.text = normalizeSnapshotPaths(payload.text);
|
|
225
|
-
if (
|
|
261
|
+
if (turn.isError || turn.failure || !adapter.sessionId())
|
|
226
262
|
throw new Error(`Preparation provider unavailable: ${turn.text}`);
|
|
227
263
|
if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
|
|
228
264
|
throw new Error("Source changed during preparation; retained dispatch requires reconciliation");
|
|
229
|
-
return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(
|
|
265
|
+
return { text: payload.text, structured: turn.structured, selectedDispatch: turn.selectedDispatch, output, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(operationPayload(turn, output), snapshot.path), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
|
|
230
266
|
}
|
|
231
267
|
finally {
|
|
232
268
|
try {
|
|
@@ -2,11 +2,13 @@ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session
|
|
|
2
2
|
import { WorkflowDb } from "./workflowDb.js";
|
|
3
3
|
import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
|
|
4
4
|
import { captureBuildSource } from "./buildAssignment.js";
|
|
5
|
-
import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields,
|
|
5
|
+
import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, validateContractCoverage } from "./qaContractCoverage.js";
|
|
6
6
|
import { contractDigest } from "./qaVerificationContract.js";
|
|
7
7
|
import { OperationDeadline } from "./util/deadline.js";
|
|
8
8
|
import { hostTestSequenceEvidence } from "./qaBuilderEvidence.js";
|
|
9
9
|
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
10
|
+
import { builderCoverageOutput, workReportOutput, remediationOutput } from "rafi-spec";
|
|
11
|
+
import { operationPayload, operationInstruction, nativePolicy } from "./structuredOperation.js";
|
|
10
12
|
/** Claims are collected after source capture so the provider can name the actual completed source. */
|
|
11
13
|
export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations = [], initialSourceDigest) {
|
|
12
14
|
return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest));
|
|
@@ -34,13 +36,19 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
// Preserve the implementation response before a failed acknowledgment or a crash.
|
|
37
|
-
const implementationDigest = db.putEvidence("qa", implementation.rawResponse ?? implementation.text);
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const implementationDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ structured: implementation.structured, rawResponse: implementation.rawResponse ?? implementation.text })));
|
|
40
|
+
// Persist host-owned command metadata before dispatching the response-only
|
|
41
|
+
// acknowledgement. This survives a process restart and does not retain
|
|
42
|
+
// commands or output, only bounded sequence/status/digest facts.
|
|
43
|
+
const hostObservationDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ version: 1, kind: "host-builder-tool-observations", workId, operationId, observations: hostObservations })));
|
|
44
|
+
db.planOperation({ runId, idempotencyKey: eventId, kind: "builder-coverage", intent: { workId, contractDigest: contract.contentDigest, binding, implementationDigest, hostObservationDigest, session } });
|
|
45
|
+
db.updateOperation(eventId, "in_progress", { result: { hostObservationDigest } });
|
|
40
46
|
try {
|
|
41
47
|
const prompt = `Response-only evidence collection for completed implementation ${operationId}. Do not run tools, modify files, repeat work, or invent verification. Record failed, blocked or not-run truthfully. The implementation response is retained as ${implementationDigest}.\n${COVERAGE_INSTRUCTIONS}\nExact coverage binding: ${JSON.stringify({ version: 1, runId, workId, revision: contract.revision, contractDigest: contract.contentDigest, ...binding })}\nAll checks: ${JSON.stringify(contract.checks)}`;
|
|
42
|
-
const
|
|
43
|
-
|
|
48
|
+
const composed = implementation.structured?.state === "candidate" ? implementation.structured.operation === "builder-remediation"
|
|
49
|
+
? operationPayload(implementation, remediationOutput).coverage : operationPayload(implementation, workReportOutput).coverage : undefined;
|
|
50
|
+
const response = composed ? implementation : await new OperationDeadline("Builder coverage acknowledgment", 120_000).run(() => dispatchWithGraphAccess(projectDir, adapter, `${prompt}\n\n${operationInstruction(builderCoverageOutput)}`, nativePolicy(builderCoverageOutput, { purpose: "response-repair", responseOnly: true, logicalActionId: operationId, journalRunId: runId }), (text, policy) => adapter.sendTurn(text, policy)), () => { void adapter.close(); });
|
|
51
|
+
const responseDigest = db.putEvidence("qa", Buffer.from(JSON.stringify({ structured: response.structured, rawResponse: response.rawResponse ?? response.text })));
|
|
44
52
|
db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
|
|
45
53
|
if (response.isError || response.failure)
|
|
46
54
|
throw new Error("Builder coverage provider failed; retained response requires reconciliation");
|
|
@@ -55,13 +63,14 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
55
63
|
// Bind the retained coverage to the host-observed post-compaction session
|
|
56
64
|
// while continuing to reject unscoped session changes and source edits.
|
|
57
65
|
binding = { ...binding, sessionId: afterSession.sessionId };
|
|
58
|
-
const
|
|
66
|
+
const claims = composed ?? operationPayload(response, builderCoverageOutput);
|
|
67
|
+
const coverage = bindCoverageHostFields(claims, contract, binding);
|
|
59
68
|
bindHostOwnedBuilderFacts(db, coverage, contract, hostObservations, initialSourceDigest, source.digest, workId, projectDir);
|
|
60
69
|
const errors = validateContractCoverage(contract, coverage, binding, false);
|
|
61
70
|
if (errors.length)
|
|
62
71
|
throw new Error(`Builder coverage invalid: ${errors.join("; ")}`);
|
|
63
72
|
const coverageDigest = store.putArtifact("builder-coverage", coverage);
|
|
64
|
-
db.updateOperation(eventId, "confirmed", { result: { responseDigest, coverageDigest, binding, implementationDigest } });
|
|
73
|
+
db.updateOperation(eventId, "confirmed", { result: { responseDigest, coverageDigest, binding, implementationDigest, hostObservationDigest } });
|
|
65
74
|
}
|
|
66
75
|
catch (error) {
|
|
67
76
|
throw new Error(`Builder evidence collection blocked: ${String(error)}`);
|
|
@@ -74,7 +83,7 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
74
83
|
function checkText(check) {
|
|
75
84
|
return JSON.stringify(check);
|
|
76
85
|
}
|
|
77
|
-
function bindHostOwnedBuilderFacts(db, coverage, contract, observations,
|
|
86
|
+
function bindHostOwnedBuilderFacts(db, coverage, contract, observations, initialSourceDigest, finalSourceDigest, workId, projectDir) {
|
|
78
87
|
const graphFact = qaHostGraphFact(projectDir);
|
|
79
88
|
const graphDisabled = /"enabled":false/.test(graphFact);
|
|
80
89
|
for (const row of coverage.checks ?? []) {
|
|
@@ -89,8 +98,8 @@ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initia
|
|
|
89
98
|
row.evidence = [...(row.evidence ?? []), { procedure: "Rafi retained preimplementation baseline observation", cwd: ".", runtime: "rafi host", sourceDigest: baseline.sourceDigest, reference: baseline.evidenceDigest }];
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
|
-
if (graphDisabled &&
|
|
93
|
-
const factDigest = db.putEvidence("qa", graphFact);
|
|
101
|
+
if (graphDisabled && check.hostKind === "graph-maintenance") {
|
|
102
|
+
const factDigest = db.putEvidence("qa", Buffer.from(graphFact));
|
|
94
103
|
if (row.outcome === "not-run")
|
|
95
104
|
row.outcome = "passed";
|
|
96
105
|
if (!row.evidence?.some(evidence => evidence.reference === `host-graph-fact:${factDigest}`))
|
|
@@ -102,11 +111,11 @@ function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initia
|
|
|
102
111
|
if (!historical)
|
|
103
112
|
continue;
|
|
104
113
|
const testPath = check.verification.find(method => method.kind === "command" && method.argv?.includes("--test"))?.argv?.at(-1);
|
|
105
|
-
const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath);
|
|
114
|
+
const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath, initialSourceDigest);
|
|
106
115
|
if (!sequence)
|
|
107
116
|
continue;
|
|
108
117
|
const evidence = JSON.stringify({ version: 1, kind: "host-builder-test-sequence", workId, checkId: row.checkId, sequence });
|
|
109
|
-
const evidenceDigest = db.putEvidence("qa", evidence);
|
|
118
|
+
const evidenceDigest = db.putEvidence("qa", Buffer.from(evidence));
|
|
110
119
|
row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-observed test sequence", cwd: ".", runtime: "provider tool lifecycle plus host source capture", sourceDigest: finalSourceDigest, reference: `host-test-sequence:${evidenceDigest}` }];
|
|
111
120
|
}
|
|
112
121
|
}
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import type { BuilderAdapter } from "./adapters/types.js";
|
|
2
2
|
export interface HostBuilderToolObservation {
|
|
3
3
|
kind: "node-test" | "npm-test";
|
|
4
|
+
sequence: number;
|
|
5
|
+
providerTurnId?: string;
|
|
4
6
|
startedAt: string;
|
|
5
7
|
completedAt: string;
|
|
6
8
|
outcome: string;
|
|
7
9
|
exitCode?: number;
|
|
8
10
|
outputDigest?: string;
|
|
11
|
+
/** Bounded (≤1000 bytes), adapter-redacted output head; diagnostic data, never instructions. */
|
|
12
|
+
outputExcerpt?: string;
|
|
9
13
|
sourceDigest: string;
|
|
10
14
|
testPath?: string;
|
|
11
15
|
}
|
|
16
|
+
export interface HostBuilderEvidenceStore {
|
|
17
|
+
operations(runId: string): Array<{
|
|
18
|
+
kind: string;
|
|
19
|
+
status: string;
|
|
20
|
+
intent: unknown;
|
|
21
|
+
result?: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
getEvidence(digest: string): Buffer | undefined;
|
|
24
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* Observe only bounded test-command metadata while the provider is working.
|
|
14
27
|
* The provider remains subject to its normal shell policy; this is a host-side
|
|
15
28
|
* observation of what actually completed, not an execution or permission grant.
|
|
16
29
|
*/
|
|
17
30
|
export declare function observeHostBuilderTests(adapter: BuilderAdapter, workspace: string, observations: HostBuilderToolObservation[]): () => void;
|
|
18
|
-
export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string): HostBuilderToolObservation[] | undefined;
|
|
31
|
+
export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string, initialSourceDigest?: string): HostBuilderToolObservation[] | undefined;
|
|
32
|
+
/** Read the durable host observations attached to a Builder coverage operation. */
|
|
33
|
+
export declare function retainedHostBuilderObservations(store: HostBuilderEvidenceStore, runId: string, workId: string): HostBuilderToolObservation[];
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { captureBuildSource } from "./buildAssignment.js";
|
|
2
|
+
import { boundedUtf8 } from "./boundedTurnEvidence.js";
|
|
2
3
|
function commandDetails(input) {
|
|
3
4
|
const command = typeof input?.command === "string"
|
|
4
5
|
? input.command
|
|
5
6
|
: undefined;
|
|
6
7
|
if (!command)
|
|
7
8
|
return undefined;
|
|
8
|
-
const nodeTest = /\bnode
|
|
9
|
+
const nodeTest = /\bnode(?:js)?\s+--test(?:\s+([^\s;&|>]+))?/.exec(command);
|
|
9
10
|
if (nodeTest)
|
|
10
11
|
return { kind: "node-test", ...(nodeTest[1] ? { testPath: nodeTest[1] } : {}) };
|
|
11
|
-
if (/\
|
|
12
|
+
if (/\b(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b/.test(command))
|
|
12
13
|
return { kind: "npm-test" };
|
|
13
14
|
return undefined;
|
|
14
15
|
}
|
|
@@ -19,12 +20,13 @@ function commandDetails(input) {
|
|
|
19
20
|
*/
|
|
20
21
|
export function observeHostBuilderTests(adapter, workspace, observations) {
|
|
21
22
|
const open = new Map();
|
|
22
|
-
|
|
23
|
+
let sequence = observations.length;
|
|
24
|
+
const listener = (event) => {
|
|
23
25
|
if (event.kind !== "tool")
|
|
24
26
|
return;
|
|
25
27
|
const details = commandDetails(event.input);
|
|
26
28
|
if (event.lifecycle === "started" && details && event.callId) {
|
|
27
|
-
open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString() });
|
|
29
|
+
open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString(), providerTurnId: event.providerTurnId });
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
32
|
if (event.lifecycle !== "completed" || !event.callId || !event.completionKnown)
|
|
@@ -37,6 +39,8 @@ export function observeHostBuilderTests(adapter, workspace, observations) {
|
|
|
37
39
|
return;
|
|
38
40
|
observations.push({
|
|
39
41
|
kind: prior.kind,
|
|
42
|
+
sequence: sequence++,
|
|
43
|
+
...(prior.providerTurnId ? { providerTurnId: prior.providerTurnId } : {}),
|
|
40
44
|
startedAt: prior.startedAt,
|
|
41
45
|
completedAt: event.observedAt ?? new Date().toISOString(),
|
|
42
46
|
// Some adapters label every finished tool call "completed" even when
|
|
@@ -45,16 +49,52 @@ export function observeHostBuilderTests(adapter, workspace, observations) {
|
|
|
45
49
|
outcome: event.exitCode !== undefined ? event.exitCode === 0 ? "completed" : "failed" : event.status ?? "completed",
|
|
46
50
|
...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
|
|
47
51
|
...(event.outputDigest && /^[a-f0-9]{64}$/.test(event.outputDigest) ? { outputDigest: event.outputDigest } : {}),
|
|
52
|
+
...(typeof event.outputSummary === "string" && event.outputSummary ? { outputExcerpt: boundedUtf8(event.outputSummary, 1000).toString() } : {}),
|
|
48
53
|
sourceDigest: captureBuildSource(workspace).digest,
|
|
49
54
|
...(prior.testPath ? { testPath: prior.testPath } : {}),
|
|
50
55
|
});
|
|
51
|
-
}
|
|
56
|
+
};
|
|
57
|
+
// This supplementary telemetry must not preempt the delivery journal's own
|
|
58
|
+
// correlated-terminal check. Wrapper adapters deliberately throw when a
|
|
59
|
+
// provider lacks observation; dispatchOwned records that as
|
|
60
|
+
// delivery-uncertain without sending a turn. Treating this optional
|
|
61
|
+
// subscription as fatal here would bypass that durable fail-closed path.
|
|
62
|
+
let stop = () => { };
|
|
63
|
+
try {
|
|
64
|
+
stop = adapter.observeEvents?.(listener) ?? stop;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
open.clear();
|
|
68
|
+
}
|
|
52
69
|
return () => { stop(); open.clear(); };
|
|
53
70
|
}
|
|
54
|
-
export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath) {
|
|
55
|
-
const relevant = observations
|
|
56
|
-
|
|
71
|
+
export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath, initialSourceDigest) {
|
|
72
|
+
const relevant = observations
|
|
73
|
+
.filter(observation => (observation.kind === "node-test" || observation.kind === "npm-test") && (!testPath || observation.testPath === testPath))
|
|
74
|
+
.sort((a, b) => a.sequence - b.sequence || a.startedAt.localeCompare(b.startedAt));
|
|
75
|
+
const red = relevant.find(observation => observation.outcome !== "completed" && observation.outcome !== "denied" && (!initialSourceDigest || observation.sourceDigest === initialSourceDigest));
|
|
57
76
|
const redIndex = red ? relevant.indexOf(red) : -1;
|
|
58
77
|
const green = red ? relevant.slice(redIndex + 1).find(observation => observation.sourceDigest === finalSourceDigest && observation.outcome === "completed" && observation.sourceDigest !== red.sourceDigest) : undefined;
|
|
59
78
|
return red && green ? [red, green] : undefined;
|
|
60
79
|
}
|
|
80
|
+
/** Read the durable host observations attached to a Builder coverage operation. */
|
|
81
|
+
export function retainedHostBuilderObservations(store, runId, workId) {
|
|
82
|
+
for (const operation of store.operations(runId).filter(item => item.kind === "builder-coverage").reverse()) {
|
|
83
|
+
const intent = operation.intent;
|
|
84
|
+
if (intent.workId !== workId)
|
|
85
|
+
continue;
|
|
86
|
+
const result = operation.result;
|
|
87
|
+
const digest = typeof result?.hostObservationDigest === "string" ? result.hostObservationDigest
|
|
88
|
+
: typeof intent.hostObservationDigest === "string" ? intent.hostObservationDigest : undefined;
|
|
89
|
+
if (!digest)
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(store.getEvidence(digest)?.toString("utf8") ?? "null");
|
|
93
|
+
if (!Array.isArray(parsed?.observations))
|
|
94
|
+
continue;
|
|
95
|
+
return parsed.observations.filter((item) => Boolean(item && typeof item === "object" && typeof item.kind === "string" && Number.isSafeInteger(item.sequence)));
|
|
96
|
+
}
|
|
97
|
+
catch { /* malformed optional evidence is not a QA pass */ }
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
|
|
2
2
|
export declare const COVERAGE_INSTRUCTIONS: string;
|
|
3
|
-
export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step.
|
|
4
|
-
export declare function parseContractCoverage(
|
|
3
|
+
export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Include the coverage claims in the completed work-report payload when a contract is supplied. Follow the contract applicability.kind exactly: an unconditional check is always applicable, including a check that verifies documentation is disabled or a skill is not requested. Such a check can pass with concrete evidence of that absence; do not mark the check itself not-applicable. Conditional checks need observed predicate evidence. Report a genuine conflict as blocked instead of silently waiving an unconditional check. Do not wait for or invent host binding values: Rafi captures and attaches the completed source after your result.";
|
|
4
|
+
export declare function parseContractCoverage(_text: string): never;
|
|
5
5
|
/** Source identity is host-owned; providers may copy a stale Builder digest. */
|
|
6
6
|
export declare function bindCoverageSourceDigest(coverage: QaContractCoverageV1, sourceDigest: string): QaContractCoverageV1;
|
|
7
7
|
/** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Ajv } from "ajv";
|
|
2
|
-
import { qaContractCoverageV1Schema } from "rafi-spec";
|
|
2
|
+
import { qaContractCoverageV1Schema, coverageClaimsSchema } from "rafi-spec";
|
|
3
3
|
const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
|
|
4
|
-
|
|
5
|
-
export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before emitting qa_pass, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
|
|
4
|
+
export const COVERAGE_INSTRUCTIONS = `Include coverage claims in the native operation payload. The claims schema is ${JSON.stringify(coverageClaimsSchema)}. Do not invent or echo host-owned run, source, attempt, revision or session bindings; Rafi attaches them after source capture. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime and reference. For postimplementation evidence, cite what you actually observed; Rafi binds the source digest. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Graphify is optional evidence: do not install, refresh, scan, or claim Graphify execution unless the host says Rafi-managed Graphify is adopted and enabled. Host-owned Graphify applicability is authoritative; disabled, unadopted, or unavailable Graphify is not a QA blocker, while adopted-enabled Graphify is applicable and cannot be waived by provider output. Other host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before reporting a passed verdict, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. For command exit status, use the host-retained tool event's exitCode and bounded output when available; that is authoritative evidence, and you must not create an echo wrapper, compound command, or other shell scaffold solely to print a numeric status. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
|
|
6
5
|
// The source and attempt binding do not exist until the implementation turn has finished.
|
|
7
|
-
export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step.
|
|
8
|
-
export function parseContractCoverage(
|
|
6
|
+
export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Include the coverage claims in the completed work-report payload when a contract is supplied. Follow the contract applicability.kind exactly: an unconditional check is always applicable, including a check that verifies documentation is disabled or a skill is not requested. Such a check can pass with concrete evidence of that absence; do not mark the check itself not-applicable. Conditional checks need observed predicate evidence. Report a genuine conflict as blocked instead of silently waiving an unconditional check. Do not wait for or invent host binding values: Rafi captures and attaches the completed source after your result.`;
|
|
7
|
+
export function parseContractCoverage(_text) { throw new Error("Legacy coverage markers cannot provide workflow authority; use the verified native payload"); }
|
|
9
8
|
/** Source identity is host-owned; providers may copy a stale Builder digest. */
|
|
10
9
|
export function bindCoverageSourceDigest(coverage, sourceDigest) {
|
|
11
10
|
const copy = structuredClone(coverage);
|
|
@@ -20,10 +19,11 @@ export function bindCoverageHostFields(coverage, contract, binding) {
|
|
|
20
19
|
// Optional schema fields may be emitted as JSON null by providers. Null is
|
|
21
20
|
// not a meaningful limitation, so omit only that representation variant;
|
|
22
21
|
// non-string values elsewhere remain validation errors.
|
|
23
|
-
for (const check of copy.checks ?? [])
|
|
22
|
+
for (const check of copy.checks ?? []) {
|
|
24
23
|
for (const evidence of check.evidence ?? [])
|
|
25
24
|
if (evidence.limitation === null)
|
|
26
25
|
delete evidence.limitation;
|
|
26
|
+
}
|
|
27
27
|
copy.version = 1;
|
|
28
28
|
copy.runId = contract.runId;
|
|
29
29
|
copy.workId = contract.workId;
|
|
@@ -61,7 +61,11 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
|
|
|
61
61
|
issues.push(`Unconditional applicability cannot be waived ${row.checkId}`);
|
|
62
62
|
if (row.applicability === "not-applicable" && !row.predicateEvidence?.length)
|
|
63
63
|
issues.push(`Not-applicable requires predicate evidence ${row.checkId}`);
|
|
64
|
-
|
|
64
|
+
// Host-owned conditional predicates are resolved by the host augmentation
|
|
65
|
+
// after provider coverage is collected (for example Graphify adoption).
|
|
66
|
+
// Requiring provider predicate evidence here would make the host-owned,
|
|
67
|
+
// fail-open path block before the authoritative host observation exists.
|
|
68
|
+
if (check.applicability.kind === "conditional" && check.applicability.decisionOwner !== "host" && row.applicability === "applicable" && !row.predicateEvidence?.length)
|
|
65
69
|
issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
|
|
66
70
|
if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
|
|
67
71
|
issues.push(`Mandatory check incomplete ${row.checkId}: run its permitted verification now and resubmit passed evidence, or report a truthful blocked result for host recovery`);
|