ai-foreman 1.7.28 → 1.7.29
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/dist/adapters/claude.js +1 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +43 -0
- package/dist/cli/start.js +13 -1
- package/dist/cli/tickets.d.ts +7 -0
- package/dist/cli/tickets.js +18 -3
- package/dist/foreman.d.ts +11 -2
- package/dist/foreman.js +22 -10
- package/dist/hostGraphApplicability.d.ts +45 -0
- package/dist/hostGraphApplicability.js +99 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +64 -0
- package/dist/hostQaComparison.js +2 -1
- package/dist/qaBuildGate.js +3 -2
- package/dist/qaContractCoverage.js +12 -3
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaPreparation.js +21 -2
- package/dist/qaPreparationStore.js +34 -6
- package/dist/qaReview.js +18 -5
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +2 -0
- package/dist/qaVerificationContract.js +37 -2
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +44 -11
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/workflowDb.js +28 -6
- package/package.json +3 -3
package/dist/adapters/claude.js
CHANGED
|
@@ -749,7 +749,7 @@ export class ClaudeAdapter {
|
|
|
749
749
|
const outputDigest = createHash("sha256").update(text).digest("hex");
|
|
750
750
|
// Persist only correlation metadata. The result text can contain source
|
|
751
751
|
// paths, command output, or credentials and must not enter lifecycle logs.
|
|
752
|
-
this.opts.onLifecycleTrace?.({ phase: "provider-tool-result", at: new Date().toISOString(), sessionId: this._sessionId, providerTurnId: this.activeProviderTurnId, toolCallId: callId, toolName: call?.name ?? "unidentified tool", classification: status, resultReason, isError: failed, outputDigest, rawOutputBytes: Buffer.byteLength(text) });
|
|
752
|
+
this.opts.onLifecycleTrace?.({ phase: "provider-tool-result", at: new Date().toISOString(), sessionId: this._sessionId, providerTurnId: this.activeProviderTurnId, toolCallId: callId, toolName: call?.name ?? "unidentified tool", classification: status, resultReason, isError: failed, outputDigest, rawOutputBytes: Buffer.byteLength(text), completionKnown: status !== "unknown" });
|
|
753
753
|
if (status === "unknown") {
|
|
754
754
|
if (this.unknownToolResults.get(callId) === outputDigest)
|
|
755
755
|
continue;
|
package/dist/adapters/types.d.ts
CHANGED
package/dist/buildApproval.d.ts
CHANGED
|
@@ -7,5 +7,20 @@ export declare function approvedBuildScope(projectDir: string, ticketIds?: reado
|
|
|
7
7
|
export declare function requiresBuildApproval(projectDir: string, ticketIds: readonly string[] | undefined, explicitYes: boolean, consequences?: Record<string, unknown>): boolean;
|
|
8
8
|
/** Bind decisions to current files so an old answer cannot approve a changed scope. */
|
|
9
9
|
export declare function buildScopeRevision(projectDir: string, consequences?: Record<string, unknown>): string;
|
|
10
|
+
export interface PlanApprovalBinding {
|
|
11
|
+
planDigest: string;
|
|
12
|
+
ticketSetDigest: string;
|
|
13
|
+
ticketDefinitionsDigest: string;
|
|
14
|
+
scopeRevision: string;
|
|
15
|
+
workflowConsequenceDigest: string;
|
|
16
|
+
}
|
|
17
|
+
/** Compute the exact host-owned inputs covered by a recovery plan approval. */
|
|
18
|
+
export declare function planApprovalBinding(projectDir: string, ticketIds: readonly string[], consequences?: Record<string, unknown>): PlanApprovalBinding;
|
|
19
|
+
export declare function validatePlanApprovalBinding(projectDir: string, ticketIds: readonly string[], approval: PlanApprovalBinding, consequences?: Record<string, unknown>): {
|
|
20
|
+
valid: true;
|
|
21
|
+
} | {
|
|
22
|
+
valid: false;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
10
25
|
/** Reuse a decision only while its source and consequences remain unchanged. */
|
|
11
26
|
export declare function createBuildApprovalGate(projectDir: string, ticketIds: readonly string[] | undefined, explicitYes: boolean, consequences: Record<string, unknown> | undefined, requestApproval: () => Promise<void>): () => Promise<void>;
|
package/dist/buildApproval.js
CHANGED
|
@@ -125,6 +125,49 @@ export function buildScopeRevision(projectDir, consequences) {
|
|
|
125
125
|
});
|
|
126
126
|
return createHash("sha256").update(JSON.stringify([contents, consequences])).digest("hex");
|
|
127
127
|
}
|
|
128
|
+
/** Compute the exact host-owned inputs covered by a recovery plan approval. */
|
|
129
|
+
export function planApprovalBinding(projectDir, ticketIds, consequences) {
|
|
130
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
131
|
+
const all = loadTickets(paths.tickets);
|
|
132
|
+
const selected = ticketIds.map(id => all.find(ticket => ticket.id === id));
|
|
133
|
+
if (selected.some(ticket => !ticket))
|
|
134
|
+
throw new Error("plan approval cannot bind a missing ticket definition");
|
|
135
|
+
const config = parse(readFileSync(join(projectDir, "rafi-config.yaml"), "utf8"));
|
|
136
|
+
const docsRoot = config.docs?.root ?? "docs";
|
|
137
|
+
const plan = JSON.parse(readFileSync(join(projectDir, docsRoot, "rafi-plan.json"), "utf8"));
|
|
138
|
+
const planDigest = createHash("sha256").update(canonical({ ...plan, content_digest: "" })).digest("hex");
|
|
139
|
+
const db = new WorkflowDb(projectDir);
|
|
140
|
+
try {
|
|
141
|
+
const approved = db.completedPlanApprovals().some(receipt => {
|
|
142
|
+
const decision = receipt.decisionReceipt;
|
|
143
|
+
return receipt.planId === plan.plan_id && receipt.revision === plan.revision
|
|
144
|
+
&& receipt.digest === planDigest && decision?.planDigest === planDigest;
|
|
145
|
+
});
|
|
146
|
+
if (!approved)
|
|
147
|
+
throw new Error("current plan has no matching completed host approval");
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
const ticketSetDigest = createHash("sha256").update(canonical([...ticketIds].sort())).digest("hex");
|
|
153
|
+
const ticketDefinitionsDigest = createHash("sha256").update(canonical(selected.map(ticket => material(ticket)).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))))).digest("hex");
|
|
154
|
+
const workflow = { build: config.tickets?.build ?? {}, consequences: consequences ?? {} };
|
|
155
|
+
const workflowConsequenceDigest = createHash("sha256").update(canonical(workflow)).digest("hex");
|
|
156
|
+
return { planDigest, ticketSetDigest, ticketDefinitionsDigest, scopeRevision: buildScopeRevision(projectDir, consequences), workflowConsequenceDigest };
|
|
157
|
+
}
|
|
158
|
+
export function validatePlanApprovalBinding(projectDir, ticketIds, approval, consequences) {
|
|
159
|
+
try {
|
|
160
|
+
const current = planApprovalBinding(projectDir, ticketIds, consequences);
|
|
161
|
+
for (const key of ["planDigest", "ticketSetDigest", "ticketDefinitionsDigest", "scopeRevision", "workflowConsequenceDigest"]) {
|
|
162
|
+
if (current[key] !== approval[key])
|
|
163
|
+
return { valid: false, reason: `${key} changed` };
|
|
164
|
+
}
|
|
165
|
+
return { valid: true };
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return { valid: false, reason: `approval inputs unavailable: ${String(error)}` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
128
171
|
/** Reuse a decision only while its source and consequences remain unchanged. */
|
|
129
172
|
export function createBuildApprovalGate(projectDir, ticketIds, explicitYes, consequences, requestApproval) {
|
|
130
173
|
let approvedRevision;
|
package/dist/cli/start.js
CHANGED
|
@@ -11,7 +11,7 @@ import { captureFrozenQaSource } from "../qaSnapshot.js";
|
|
|
11
11
|
import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
|
|
12
12
|
import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery, assertInterruptedSourceSnapshot } from "../buildAssignment.js";
|
|
13
13
|
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
14
|
-
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.js";
|
|
14
|
+
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate, validatePlanApprovalBinding } from "../buildApproval.js";
|
|
15
15
|
import { Command, Option } from "commander";
|
|
16
16
|
import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
17
17
|
import { resolve, join, relative } from "node:path";
|
|
@@ -693,6 +693,18 @@ export function buildStartCommand(options = {}) {
|
|
|
693
693
|
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
694
694
|
&& (recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-yes" || recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-choice")
|
|
695
695
|
: Boolean(opts.yes);
|
|
696
|
+
const recoveryTickets = recoveryRecord?.recoveryDecision?.executionTickets ?? recoveryRecord?.tickets ?? [];
|
|
697
|
+
// Empty-scope recovery is used by established/supervisor lifecycle
|
|
698
|
+
// recovery fixtures and has no ticket plan to bind. Ticket-bearing
|
|
699
|
+
// recovery remains fail-closed on an exact current-plan approval.
|
|
700
|
+
if (recoveryRecord && autoApprovePlanUpdates && recoveryTickets.length > 0) {
|
|
701
|
+
const decision = recoveryRecord.recoveryDecision;
|
|
702
|
+
if (!decision?.planApproval)
|
|
703
|
+
fail("saved --yes approval has no exact plan binding; rerun rafi build:resume --yes to approve the current plan");
|
|
704
|
+
const approvalCheck = validatePlanApprovalBinding(cwd, recoveryTickets, decision.planApproval);
|
|
705
|
+
if (!approvalCheck.valid)
|
|
706
|
+
fail(`saved --yes approval is stale (${approvalCheck.reason}); rerun rafi build:resume --yes before provider work`);
|
|
707
|
+
}
|
|
696
708
|
if (opts.recoveryMode) {
|
|
697
709
|
const allowed = ["exact-session", "fresh-with-handoff", "fresh-recovery-only", "guided-recovery"];
|
|
698
710
|
if (!allowed.includes(String(opts.recoveryMode)))
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { StructuredPlanV1 } from "rafi-spec";
|
|
3
3
|
import { Log } from "../log.js";
|
|
4
|
+
import { type StepStatus } from "../foreman.js";
|
|
4
5
|
import type { EffortLevel } from "../adapters/types.js";
|
|
5
6
|
import { type RoleBuilderOptions } from "../agentRun.js";
|
|
6
7
|
import type { TicketsConfig } from "../tickets/config.js";
|
|
7
8
|
import { type TicketPopulateRuntimeMode, type TicketSourceConfig, type TicketsSetupConfig } from "../tickets/setupConfig.js";
|
|
9
|
+
/**
|
|
10
|
+
* Ticket Maker may append an explanatory note after its machine envelope.
|
|
11
|
+
* Preserve a schema-valid proposal before Foreman's strict generic marker
|
|
12
|
+
* repair path replaces the response with a status-only correction.
|
|
13
|
+
*/
|
|
14
|
+
export declare function acceptTicketPopulationCompletion(text: string): StepStatus | undefined;
|
|
8
15
|
/** Commander preserves comma-delimited values as one argument; accept both documented forms. */
|
|
9
16
|
export declare function normalizeRiskWaiverTicketIds(values: readonly string[] | undefined): string[];
|
|
10
17
|
/** Record an explicit, definition-bound exception for existing risk tickets. */
|
package/dist/cli/tickets.js
CHANGED
|
@@ -7,7 +7,7 @@ import { riskNoteRepairProposalV1Schema, zodIssues } from "rafi-spec";
|
|
|
7
7
|
import { select, text, confirm, isCancel, multiselect } from "@clack/prompts";
|
|
8
8
|
import { loadConfig } from "../config.js";
|
|
9
9
|
import { Log } from "../log.js";
|
|
10
|
-
import { Foreman } from "../foreman.js";
|
|
10
|
+
import { Foreman, parseStepStatus } from "../foreman.js";
|
|
11
11
|
import { printEvents } from "./events.js";
|
|
12
12
|
import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDiscover, cmdAcceptFutureWork, cmdReorder, cmdRender, cmdValidate, cmdQueue, cmdStackQueue, cmdArchive, cmdReview, } from "../tickets/commands.js";
|
|
13
13
|
import { loadDeliveryConfig } from "../tickets/delivery.js";
|
|
@@ -35,6 +35,21 @@ let nestedTicketCommandDepth = 0;
|
|
|
35
35
|
class TicketInterviewCancelled extends Error {
|
|
36
36
|
}
|
|
37
37
|
function cancelTicketInterview() { throw new TicketInterviewCancelled("ticket interview cancelled"); }
|
|
38
|
+
/**
|
|
39
|
+
* Ticket Maker may append an explanatory note after its machine envelope.
|
|
40
|
+
* Preserve a schema-valid proposal before Foreman's strict generic marker
|
|
41
|
+
* repair path replaces the response with a status-only correction.
|
|
42
|
+
*/
|
|
43
|
+
export function acceptTicketPopulationCompletion(text) {
|
|
44
|
+
try {
|
|
45
|
+
extractTicketPopulationProposal(text);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const status = parseStepStatus(text, { requireFinalLine: false });
|
|
51
|
+
return status.kind === "done" || status.kind === "plan_complete" ? status : undefined;
|
|
52
|
+
}
|
|
38
53
|
function fail(msg) {
|
|
39
54
|
if (nestedTicketCommandDepth > 0)
|
|
40
55
|
throw new Error(msg);
|
|
@@ -208,7 +223,7 @@ Output one JSON object between ${TICKET_POPULATION_PROPOSAL_START} and ${TICKET_
|
|
|
208
223
|
{"version":1,"plan_id":"approved plan ID","revision":1,"tickets":[{"slice_ref":"slc_low","title":"...","area":"...","priority":"P1","size":"S","risk":"Low","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":[],"rollback":null,"notes":null},{"slice_ref":"slc_medium","title":"...","area":"...","priority":"P1","size":"S","risk":"Medium","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":["slc_low"],"rollback":"Disable the feature flag, restore the previous handler, and verify the affected error rate returns to baseline.","notes":null}],"retirements":["exact existing ticket ID"]}
|
|
209
224
|
Triple-check that every approved slice appears exactly once.
|
|
210
225
|
|
|
211
|
-
End with exactly one marker line as the final non-empty line:
|
|
226
|
+
End with exactly one marker line as the final non-empty line; do not append notes or commentary after it:
|
|
212
227
|
STEP_STATUS: done | summary="populated Foreman tickets from existing project ticket sources"
|
|
213
228
|
or
|
|
214
229
|
STEP_STATUS: blocked | reason="why ticket population cannot proceed"`;
|
|
@@ -1465,7 +1480,7 @@ export async function cmdPopulateCli(opts) {
|
|
|
1465
1480
|
if (contextSources.length) {
|
|
1466
1481
|
console.log(`foreman tickets: context sources ${contextSources.join(" ")}`);
|
|
1467
1482
|
}
|
|
1468
|
-
let turn = await foreman.runInstruction(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc));
|
|
1483
|
+
let turn = await foreman.runInstruction(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc), { completion: acceptTicketPopulationCompletion });
|
|
1469
1484
|
planningRecord = checkpointInterview(dir, planningRecord, { runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() }, decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text } });
|
|
1470
1485
|
log.write("ticket-populate", {
|
|
1471
1486
|
statusKind: turn.status.kind,
|
package/dist/foreman.d.ts
CHANGED
|
@@ -20,11 +20,18 @@ export interface StepStatus {
|
|
|
20
20
|
branchDependency?: string;
|
|
21
21
|
error?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Workflow-specific completion is opt-in. Callers may preserve a valid
|
|
25
|
+
* structured response before the generic strict STEP_STATUS repair path runs.
|
|
26
|
+
*/
|
|
27
|
+
export type InstructionCompletion = (text: string) => StepStatus | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* Pull the STEP_STATUS marker out of a turn's final text.
|
|
25
30
|
* Format: `STEP_STATUS: <kind> | key="value" key="value"`.
|
|
26
31
|
*/
|
|
27
|
-
export declare function parseStepStatus(text: string
|
|
32
|
+
export declare function parseStepStatus(text: string, options?: {
|
|
33
|
+
requireFinalLine?: boolean;
|
|
34
|
+
}): StepStatus;
|
|
28
35
|
/** Heuristic: did a marker-less turn end by asking the human something? */
|
|
29
36
|
export declare function looksLikeQuestion(text: string): boolean;
|
|
30
37
|
export { MARKER_SPEC } from "./markers.js";
|
|
@@ -126,7 +133,9 @@ export declare class Foreman {
|
|
|
126
133
|
/** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
|
|
127
134
|
sendPreflightFeedback(feedback: string): Promise<void>;
|
|
128
135
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
129
|
-
runInstruction(instruction: string
|
|
136
|
+
runInstruction(instruction: string, options?: {
|
|
137
|
+
completion?: InstructionCompletion;
|
|
138
|
+
}): Promise<{
|
|
130
139
|
result: TurnResult;
|
|
131
140
|
status: StepStatus;
|
|
132
141
|
}>;
|
package/dist/foreman.js
CHANGED
|
@@ -60,7 +60,7 @@ const QUESTION_HINTS = [
|
|
|
60
60
|
* Pull the STEP_STATUS marker out of a turn's final text.
|
|
61
61
|
* Format: `STEP_STATUS: <kind> | key="value" key="value"`.
|
|
62
62
|
*/
|
|
63
|
-
export function parseStepStatus(text) {
|
|
63
|
+
export function parseStepStatus(text, options = {}) {
|
|
64
64
|
const markerLines = text.split(/\r?\n/).filter((line) => /^\s*STEP_STATUS:/.test(line));
|
|
65
65
|
const markerCount = markerLines.length;
|
|
66
66
|
if (markerCount > 1) {
|
|
@@ -69,12 +69,14 @@ export function parseStepStatus(text) {
|
|
|
69
69
|
const lines = text.trimEnd().split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
70
70
|
const last = lines[lines.length - 1] ?? "";
|
|
71
71
|
if (!last.includes("STEP_STATUS:")) {
|
|
72
|
-
if (markerCount === 1) {
|
|
72
|
+
if (markerCount === 1 && options.requireFinalLine !== false) {
|
|
73
73
|
return { kind: "unknown", error: "STEP_STATUS marker was not the final non-empty line" };
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
if (markerCount === 0)
|
|
76
|
+
return { kind: "unknown" };
|
|
76
77
|
}
|
|
77
|
-
const
|
|
78
|
+
const marker = options.requireFinalLine === false ? markerLines[0]?.trim() ?? "" : last;
|
|
79
|
+
const match = marker.match(/^STEP_STATUS:\s*(done|blocked|plan_complete|needs_input|qa_pass|qa_fail)\b\s*(?:\|\s*(.*))?$/i);
|
|
78
80
|
if (!match) {
|
|
79
81
|
return { kind: "unknown", error: "malformed STEP_STATUS marker" };
|
|
80
82
|
}
|
|
@@ -438,12 +440,12 @@ export class Foreman {
|
|
|
438
440
|
this.builder = await this.builderFactory(this.projectDir);
|
|
439
441
|
this.log.write("branch-session", { role: "builder", transition: "missing-session-fresh", continuityLost: true });
|
|
440
442
|
}
|
|
441
|
-
async doTurnWith(adapter, instruction, mode = "builder") {
|
|
443
|
+
async doTurnWith(adapter, instruction, mode = "builder", completion) {
|
|
442
444
|
return this.projectDir
|
|
443
|
-
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode))
|
|
444
|
-
: this.doTurnWithOwned(adapter, instruction, mode);
|
|
445
|
+
? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode, completion))
|
|
446
|
+
: this.doTurnWithOwned(adapter, instruction, mode, completion);
|
|
445
447
|
}
|
|
446
|
-
async doTurnWithOwned(adapter, instruction, mode = "builder") {
|
|
448
|
+
async doTurnWithOwned(adapter, instruction, mode = "builder", completion) {
|
|
447
449
|
const ticket = mode === "builder" ? this.currentTicketId : undefined;
|
|
448
450
|
const hostToolObservations = [];
|
|
449
451
|
const scopedInstruction = (text) => !ticket || text.includes(`Ticket scope: ${ticket}.`) ? text
|
|
@@ -498,6 +500,11 @@ export class Foreman {
|
|
|
498
500
|
let result = await send(instruction, turnPolicy);
|
|
499
501
|
await this.observeBuilderNative(adapter);
|
|
500
502
|
let status = parseStepStatus(result.text);
|
|
503
|
+
if (!result.isError && !result.failure) {
|
|
504
|
+
const completed = completion?.(result.text);
|
|
505
|
+
if (completed)
|
|
506
|
+
return { result, status: completed };
|
|
507
|
+
}
|
|
501
508
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
502
509
|
return { result, status };
|
|
503
510
|
if (result.isError || result.failure)
|
|
@@ -626,6 +633,11 @@ export class Foreman {
|
|
|
626
633
|
if (result.isError || result.failure)
|
|
627
634
|
return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
|
|
628
635
|
status = parseStepStatus(result.text);
|
|
636
|
+
if (completion) {
|
|
637
|
+
const completed = completion(result.text);
|
|
638
|
+
if (completed)
|
|
639
|
+
return { result, status: completed };
|
|
640
|
+
}
|
|
629
641
|
if (ticket && validateBuildAssignment(ticket, status))
|
|
630
642
|
return { result, status };
|
|
631
643
|
}
|
|
@@ -710,8 +722,8 @@ export class Foreman {
|
|
|
710
722
|
this.preflightPlan = result.text;
|
|
711
723
|
}
|
|
712
724
|
/** Send one custom instruction through the same needs_input loop as a batch turn. */
|
|
713
|
-
async runInstruction(instruction) {
|
|
714
|
-
return this.
|
|
725
|
+
async runInstruction(instruction, options = {}) {
|
|
726
|
+
return this.doTurnWith(this.builder, instruction, "builder", options.completion);
|
|
715
727
|
}
|
|
716
728
|
/** Ask an already-active role session to propose choices for its reported blocker. */
|
|
717
729
|
async resolveBlocker(adapter, reason, mode = "builder") {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { QaContractCoverageV1, QaVerificationContractV1, QaContractCandidateV1 } from "rafi-spec";
|
|
3
|
+
export declare const hostGraphApplicabilityV1Schema: z.ZodObject<{
|
|
4
|
+
version: z.ZodLiteral<1>;
|
|
5
|
+
runId: z.ZodString;
|
|
6
|
+
ticketId: z.ZodString;
|
|
7
|
+
contractDigest: z.ZodString;
|
|
8
|
+
contractRevision: z.ZodNumber;
|
|
9
|
+
reviewAttemptId: z.ZodString;
|
|
10
|
+
reviewBasisDigest: z.ZodString;
|
|
11
|
+
sourceDigest: z.ZodString;
|
|
12
|
+
status: z.ZodEnum<{
|
|
13
|
+
unavailable: "unavailable";
|
|
14
|
+
disabled: "disabled";
|
|
15
|
+
unadopted: "unadopted";
|
|
16
|
+
"adopted-enabled": "adopted-enabled";
|
|
17
|
+
}>;
|
|
18
|
+
reason: z.ZodString;
|
|
19
|
+
observedAt: z.ZodString;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type HostGraphApplicabilityV1 = z.infer<typeof hostGraphApplicabilityV1Schema>;
|
|
22
|
+
export declare function isGraphQaCheck(check: QaVerificationContractV1["checks"][number]): boolean;
|
|
23
|
+
export declare function findGraphQaChecks(contract: QaVerificationContractV1): QaVerificationContractV1["checks"];
|
|
24
|
+
export declare function normalizeGraphQaChecks(candidate: QaContractCandidateV1): QaContractCandidateV1;
|
|
25
|
+
export declare function createHostGraphApplicability(input: {
|
|
26
|
+
projectDir: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
ticketId: string;
|
|
29
|
+
contract: QaVerificationContractV1;
|
|
30
|
+
reviewAttemptId: string;
|
|
31
|
+
reviewBasisDigest: string;
|
|
32
|
+
sourceDigest: string;
|
|
33
|
+
store: {
|
|
34
|
+
artifacts(kind: string): Array<{
|
|
35
|
+
digest: string;
|
|
36
|
+
value: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
putArtifact(kind: string, value: unknown): string;
|
|
39
|
+
};
|
|
40
|
+
}): {
|
|
41
|
+
result: HostGraphApplicabilityV1;
|
|
42
|
+
resultDigest: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function applyHostGraphApplicability(coverage: QaContractCoverageV1, contract: QaVerificationContractV1, result: HostGraphApplicabilityV1, resultDigest: string, projectDir: string): QaContractCoverageV1;
|
|
45
|
+
export declare function hostGraphApplicabilityDigestFromCoverage(coverage: QaContractCoverageV1, checkId: string): string | undefined;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { loadGraphConfig } from "./graph/config.js";
|
|
3
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
4
|
+
export const hostGraphApplicabilityV1Schema = z.object({
|
|
5
|
+
version: z.literal(1),
|
|
6
|
+
runId: z.string().min(1),
|
|
7
|
+
ticketId: z.string().min(1),
|
|
8
|
+
contractDigest: digest,
|
|
9
|
+
contractRevision: z.number().int().positive(),
|
|
10
|
+
reviewAttemptId: z.string().min(1),
|
|
11
|
+
reviewBasisDigest: digest,
|
|
12
|
+
sourceDigest: digest,
|
|
13
|
+
status: z.enum(["adopted-enabled", "disabled", "unadopted", "unavailable"]),
|
|
14
|
+
reason: z.string().max(2048),
|
|
15
|
+
observedAt: z.string().datetime(),
|
|
16
|
+
});
|
|
17
|
+
function graphText(check) {
|
|
18
|
+
return [check.expectedBehavior, ...check.expectedEvidence, ...check.applicability.requiredEvidence,
|
|
19
|
+
...check.verification.flatMap(method => [method.expectedOutcome, ...(method.steps ?? []), ...(method.argv ?? [])])].join(" ");
|
|
20
|
+
}
|
|
21
|
+
export function isGraphQaCheck(check) {
|
|
22
|
+
return /\bgraphify(?:y)?\b|\brafi[- ]managed graph(?:ify)?\b|\bgraphify adoption\b|\bgraphify[- ](?:receipt|evidence|command)\b/i.test(graphText(check));
|
|
23
|
+
}
|
|
24
|
+
export function findGraphQaChecks(contract) {
|
|
25
|
+
return contract.checks.filter(isGraphQaCheck);
|
|
26
|
+
}
|
|
27
|
+
export function normalizeGraphQaChecks(candidate) {
|
|
28
|
+
const copy = structuredClone(candidate);
|
|
29
|
+
for (const check of copy.checks) {
|
|
30
|
+
if (!isGraphQaCheck(check))
|
|
31
|
+
continue;
|
|
32
|
+
check.origin = "invariant";
|
|
33
|
+
check.obligation = "mandatory";
|
|
34
|
+
check.timing = "postimplementation";
|
|
35
|
+
check.applicability = {
|
|
36
|
+
kind: "conditional",
|
|
37
|
+
predicate: "Rafi-managed Graphify is adopted and enabled for this project",
|
|
38
|
+
decisionOwner: "host",
|
|
39
|
+
requiredEvidence: ["Host-owned Graphify applicability observation"],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return copy;
|
|
43
|
+
}
|
|
44
|
+
export function createHostGraphApplicability(input) {
|
|
45
|
+
let status = "unavailable";
|
|
46
|
+
let reason = "Rafi-managed Graphify applicability could not be observed; Graphify is optional and direct source inspection remains authoritative.";
|
|
47
|
+
try {
|
|
48
|
+
const graph = loadGraphConfig(input.projectDir);
|
|
49
|
+
status = graph.enabled ? "adopted-enabled" : graph.reason === "disabled" ? "disabled" : "unadopted";
|
|
50
|
+
reason = graph.enabled
|
|
51
|
+
? "Rafi-managed Graphify is explicitly adopted and enabled."
|
|
52
|
+
: `Rafi-managed Graphify is not required: ${graph.reason}.`;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
reason = `Rafi-managed Graphify applicability is unavailable (${error instanceof Error ? error.message.slice(0, 400) : String(error).slice(0, 400)}); direct source inspection remains authoritative.`;
|
|
56
|
+
}
|
|
57
|
+
const result = hostGraphApplicabilityV1Schema.parse({ version: 1, runId: input.runId, ticketId: input.ticketId, contractDigest: input.contract.contentDigest, contractRevision: input.contract.revision, reviewAttemptId: input.reviewAttemptId, reviewBasisDigest: input.reviewBasisDigest, sourceDigest: input.sourceDigest, status, reason, observedAt: new Date().toISOString() });
|
|
58
|
+
const existing = input.store.artifacts("host-graph-applicability").find(item => {
|
|
59
|
+
const parsed = hostGraphApplicabilityV1Schema.safeParse(item.value);
|
|
60
|
+
return parsed.success && JSON.stringify(parsed.data) === JSON.stringify(result);
|
|
61
|
+
});
|
|
62
|
+
return { result, resultDigest: existing?.digest ?? input.store.putArtifact("host-graph-applicability", result) };
|
|
63
|
+
}
|
|
64
|
+
export function applyHostGraphApplicability(coverage, contract, result, resultDigest, projectDir) {
|
|
65
|
+
const parsed = hostGraphApplicabilityV1Schema.parse(result);
|
|
66
|
+
if (parsed.contractDigest !== contract.contentDigest || parsed.contractRevision !== contract.revision || parsed.runId !== contract.runId || parsed.ticketId !== contract.workId || parsed.sourceDigest !== coverage.sourceDigest || parsed.reviewAttemptId !== coverage.attemptId || parsed.reviewBasisDigest !== coverage.inputBasisDigest)
|
|
67
|
+
throw new Error("Host Graphify applicability is not bound to the current contract, review, or source");
|
|
68
|
+
const checks = findGraphQaChecks(contract);
|
|
69
|
+
if (!checks.length)
|
|
70
|
+
return coverage;
|
|
71
|
+
const copy = structuredClone(coverage);
|
|
72
|
+
for (const check of checks) {
|
|
73
|
+
const index = copy.checks.findIndex(row => row.checkId === check.id);
|
|
74
|
+
if (parsed.status !== "adopted-enabled") {
|
|
75
|
+
const row = {
|
|
76
|
+
checkId: check.id,
|
|
77
|
+
applicability: "not-applicable",
|
|
78
|
+
predicateEvidence: [`Host-owned Graphify applicability: ${parsed.status}. ${parsed.reason}`],
|
|
79
|
+
outcome: "passed",
|
|
80
|
+
evidence: [{ procedure: "Rafi host-owned Graphify applicability observation", cwd: projectDir, runtime: "Rafi host", sourceDigest: parsed.sourceDigest, reference: `host-graph-applicability:${resultDigest}` }],
|
|
81
|
+
};
|
|
82
|
+
if (index < 0)
|
|
83
|
+
copy.checks.push(row);
|
|
84
|
+
else
|
|
85
|
+
copy.checks[index] = row;
|
|
86
|
+
}
|
|
87
|
+
else if (index >= 0) {
|
|
88
|
+
copy.checks[index].applicability = "applicable";
|
|
89
|
+
copy.checks[index].predicateEvidence = [...new Set([...(copy.checks[index].predicateEvidence ?? []), "Host-owned Graphify applicability: adopted-enabled."])];
|
|
90
|
+
copy.checks[index].evidence.push({ procedure: "Rafi host-owned Graphify applicability observation", cwd: projectDir, runtime: "Rafi host", sourceDigest: parsed.sourceDigest, reference: `host-graph-applicability:${resultDigest}` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return copy;
|
|
94
|
+
}
|
|
95
|
+
export function hostGraphApplicabilityDigestFromCoverage(coverage, checkId) {
|
|
96
|
+
const reference = coverage.checks.find(row => row.checkId === checkId)?.evidence.find(item => item.reference.startsWith("host-graph-applicability:"))?.reference;
|
|
97
|
+
const value = reference?.slice("host-graph-applicability:".length);
|
|
98
|
+
return value && /^[a-f0-9]{64}$/.test(value) ? value : undefined;
|
|
99
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
|
|
2
|
+
import { type HostQaComparisonV1 } from "./hostQaComparison.js";
|
|
3
|
+
import { type HostGraphApplicabilityV1 } from "./hostGraphApplicability.js";
|
|
4
|
+
/** Applies every host-owned coverage decision in one canonical order. */
|
|
5
|
+
export declare function applyHostQaAugmentations(input: {
|
|
6
|
+
coverage: QaContractCoverageV1;
|
|
7
|
+
contract: QaVerificationContractV1;
|
|
8
|
+
projectDir: string;
|
|
9
|
+
hostComparison?: {
|
|
10
|
+
result: HostQaComparisonV1;
|
|
11
|
+
resultDigest: string;
|
|
12
|
+
};
|
|
13
|
+
hostGraph?: {
|
|
14
|
+
result: HostGraphApplicabilityV1;
|
|
15
|
+
resultDigest: string;
|
|
16
|
+
};
|
|
17
|
+
commandObservations?: unknown[];
|
|
18
|
+
}): QaContractCoverageV1;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { applyHostQaComparison } from "./hostQaComparison.js";
|
|
2
|
+
import { applyHostGraphApplicability } from "./hostGraphApplicability.js";
|
|
3
|
+
function commandObservationFor(check, observations) {
|
|
4
|
+
const command = check.verification.find(method => method.kind === "command");
|
|
5
|
+
const argv = command?.argv ?? [];
|
|
6
|
+
const kind = argv[0] === "npm" && argv[1] === "test" ? "npm-test" : argv[0] === "node" && argv[1] === "--test" ? "node-test" : undefined;
|
|
7
|
+
if (!kind)
|
|
8
|
+
return undefined;
|
|
9
|
+
return observations.find(observation => observation.kind === kind && observation.outcome === "completed" && observation.exitCode === 0 && typeof observation.spanId === "string");
|
|
10
|
+
}
|
|
11
|
+
function applyHostCommandObservations(coverage, contract, observations) {
|
|
12
|
+
const parsed = observations.filter((observation) => Boolean(observation && typeof observation === "object"));
|
|
13
|
+
const copy = structuredClone(coverage);
|
|
14
|
+
for (const check of contract.checks) {
|
|
15
|
+
const observation = commandObservationFor(check, parsed);
|
|
16
|
+
if (!observation)
|
|
17
|
+
continue;
|
|
18
|
+
const row = copy.checks.find(item => item.checkId === check.id);
|
|
19
|
+
if (!row || row.outcome === "failed")
|
|
20
|
+
continue;
|
|
21
|
+
row.outcome = "passed";
|
|
22
|
+
row.evidence = [...(row.evidence ?? []), {
|
|
23
|
+
procedure: "Rafi host-retained command observation",
|
|
24
|
+
cwd: ".",
|
|
25
|
+
runtime: "Rafi host",
|
|
26
|
+
sourceDigest: copy.sourceDigest,
|
|
27
|
+
reference: `host-command-span:${observation.spanId}`,
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
return copy;
|
|
31
|
+
}
|
|
32
|
+
function applyBaselineAuthorities(coverage, contract) {
|
|
33
|
+
const copy = structuredClone(coverage);
|
|
34
|
+
for (const check of contract.checks) {
|
|
35
|
+
if (check.timing !== "preimplementation")
|
|
36
|
+
continue;
|
|
37
|
+
const baseline = contract.baseline.find(observation => observation.checkId === check.id && observation.status === "passed");
|
|
38
|
+
const row = copy.checks.find(item => item.checkId === check.id);
|
|
39
|
+
if (!baseline || !row || row.applicability !== "applicable" || row.outcome !== "passed")
|
|
40
|
+
continue;
|
|
41
|
+
if (row.evidence?.some(evidence => evidence.reference === baseline.evidenceDigest))
|
|
42
|
+
continue;
|
|
43
|
+
row.evidence = [...(row.evidence ?? []), {
|
|
44
|
+
procedure: "Rafi host-retained preimplementation baseline authority",
|
|
45
|
+
cwd: ".",
|
|
46
|
+
runtime: "Rafi host",
|
|
47
|
+
sourceDigest: copy.sourceDigest,
|
|
48
|
+
reference: baseline.evidenceDigest,
|
|
49
|
+
}];
|
|
50
|
+
}
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
|
53
|
+
/** Applies every host-owned coverage decision in one canonical order. */
|
|
54
|
+
export function applyHostQaAugmentations(input) {
|
|
55
|
+
let coverage = input.coverage;
|
|
56
|
+
coverage = applyBaselineAuthorities(coverage, input.contract);
|
|
57
|
+
if (input.commandObservations?.length)
|
|
58
|
+
coverage = applyHostCommandObservations(coverage, input.contract, input.commandObservations);
|
|
59
|
+
if (input.hostComparison)
|
|
60
|
+
coverage = applyHostQaComparison(coverage, input.contract, input.hostComparison.result, input.hostComparison.resultDigest, input.projectDir);
|
|
61
|
+
if (input.hostGraph)
|
|
62
|
+
coverage = applyHostGraphApplicability(coverage, input.contract, input.hostGraph.result, input.hostGraph.resultDigest, input.projectDir);
|
|
63
|
+
return coverage;
|
|
64
|
+
}
|
package/dist/hostQaComparison.js
CHANGED
|
@@ -49,8 +49,9 @@ function isHostQaCheck(check) {
|
|
|
49
49
|
const text = `${check.expectedBehavior} ${check.expectedEvidence.join(" ")} ${check.applicability.requiredEvidence.join(" ")}`;
|
|
50
50
|
const hostScopeEvidence = check.applicability.requiredEvidence.some(evidence => /host[- ]owned.*scope comparison/i.test(evidence));
|
|
51
51
|
const scopeSemantics = /scope/i.test(text) && /comparison/i.test(text) && /baseline|protected|approved scope|product changes/i.test(text);
|
|
52
|
+
const changedProductPaths = /changed product paths/i.test(text) && /host[- ]retained|baseline|bookkeeping|evidence exclusions/i.test(text);
|
|
52
53
|
return !/documentation/i.test(text)
|
|
53
|
-
&& (hostScopeEvidence || (check.applicability.decisionOwner === "host" && (/Rafi compares the final product paths/i.test(check.expectedBehavior) || scopeSemantics)));
|
|
54
|
+
&& (hostScopeEvidence || changedProductPaths || (check.applicability.decisionOwner === "host" && (/Rafi compares the final product paths/i.test(check.expectedBehavior) || scopeSemantics)));
|
|
54
55
|
}
|
|
55
56
|
export function findHostQaCheck(contract) {
|
|
56
57
|
return contract.checks.find(isHostQaCheck);
|
package/dist/qaBuildGate.js
CHANGED
|
@@ -14,7 +14,7 @@ import { WorkflowDb } from "./workflowDb.js";
|
|
|
14
14
|
import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
|
|
15
15
|
import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
|
|
16
16
|
import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
|
|
17
|
-
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
|
|
17
|
+
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, ensureHostScopeCheck, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
|
|
18
18
|
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
19
19
|
import { prepareVerificationContract } from "./qaPreparation.js";
|
|
20
20
|
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
@@ -23,6 +23,7 @@ import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
|
|
|
23
23
|
import { renderBuildWorkContext } from "./buildWorkContext.js";
|
|
24
24
|
import { compareContractInputs } from "./qaContractFreshness.js";
|
|
25
25
|
import { readBuildRuns } from "./buildRuns.js";
|
|
26
|
+
import { normalizeGraphQaChecks } from "./hostGraphApplicability.js";
|
|
26
27
|
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
27
28
|
export function preparationTurnPayload(turn) {
|
|
28
29
|
const formatErrors = turn.continuityErrors ?? [];
|
|
@@ -129,7 +130,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
129
130
|
provider: builder.agent,
|
|
130
131
|
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
132
|
normalizeDraft: (draft, response) => {
|
|
132
|
-
const normalized =
|
|
133
|
+
const normalized = allocateStableCheckIds(bindUniqueBaselineCheckIds(rewriteProviderHostScopeChecks(normalizeGraphQaChecks(ensureHostScopeCheck(draft, hostBaseline.id)), hostBaseline.id)));
|
|
133
134
|
if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
|
|
134
135
|
normalized.baseline.unshift(structuredClone(hostBaseline));
|
|
135
136
|
for (const check of normalized.checks)
|
|
@@ -2,7 +2,7 @@ import { Ajv } from "ajv";
|
|
|
2
2
|
import { qaContractCoverageV1Schema } from "rafi-spec";
|
|
3
3
|
const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
|
|
4
4
|
import { parsePreparationArtifact } from "./qaVerificationContract.js";
|
|
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.`;
|
|
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. 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 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. 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
6
|
// The source and attempt binding do not exist until the implementation turn has finished.
|
|
7
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. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.`;
|
|
8
8
|
export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
|
|
@@ -20,10 +20,15 @@ export function bindCoverageHostFields(coverage, contract, binding) {
|
|
|
20
20
|
// Optional schema fields may be emitted as JSON null by providers. Null is
|
|
21
21
|
// not a meaningful limitation, so omit only that representation variant;
|
|
22
22
|
// non-string values elsewhere remain validation errors.
|
|
23
|
-
for (const check of copy.checks ?? [])
|
|
23
|
+
for (const check of copy.checks ?? []) {
|
|
24
|
+
// Some providers mirror the applicability token into outcome. Canonicalize
|
|
25
|
+
// that harmless representation mistake at the host boundary.
|
|
26
|
+
if (check.outcome === "not-applicable")
|
|
27
|
+
check.outcome = "not-run";
|
|
24
28
|
for (const evidence of check.evidence ?? [])
|
|
25
29
|
if (evidence.limitation === null)
|
|
26
30
|
delete evidence.limitation;
|
|
31
|
+
}
|
|
27
32
|
copy.version = 1;
|
|
28
33
|
copy.runId = contract.runId;
|
|
29
34
|
copy.workId = contract.workId;
|
|
@@ -61,7 +66,11 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
|
|
|
61
66
|
issues.push(`Unconditional applicability cannot be waived ${row.checkId}`);
|
|
62
67
|
if (row.applicability === "not-applicable" && !row.predicateEvidence?.length)
|
|
63
68
|
issues.push(`Not-applicable requires predicate evidence ${row.checkId}`);
|
|
64
|
-
|
|
69
|
+
// Host-owned conditional predicates are resolved by the host augmentation
|
|
70
|
+
// after provider coverage is collected (for example Graphify adoption).
|
|
71
|
+
// Requiring provider predicate evidence here would make the host-owned,
|
|
72
|
+
// fail-open path block before the authoritative host observation exists.
|
|
73
|
+
if (check.applicability.kind === "conditional" && check.applicability.decisionOwner !== "host" && row.applicability === "applicable" && !row.predicateEvidence?.length)
|
|
65
74
|
issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
|
|
66
75
|
if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
|
|
67
76
|
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`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedAgentSettings } from "rafi-spec";
|
|
2
2
|
export declare const FINAL_QA_ROLE_SUFFIX = "You are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.";
|
|
3
|
+
export declare const QA_COMMAND_DISCIPLINE: string;
|
|
3
4
|
export declare function finalQaRoleInstructions(rules: string): string;
|
|
4
5
|
export declare function loadQaPreparationPolicy(projectDir: string): import("special-agents").QaPreparationConfigV1;
|
|
5
6
|
/** Resolve only explicit rule/skill content; never project secrets or mutable .rafi state. */
|