ai-foreman 1.7.27 → 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.d.ts +2 -1
- package/dist/cli/start.js +35 -5
- package/dist/cli/tickets.d.ts +7 -0
- package/dist/cli/tickets.js +18 -3
- package/dist/config.js +13 -4
- 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.d.ts +50 -0
- package/dist/hostQaComparison.js +196 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -1
- package/dist/permissions/policy.js +11 -0
- package/dist/qaBuildGate.js +3 -2
- package/dist/qaBuilderCoverage.js +11 -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.d.ts +4 -0
- package/dist/qaPreparationStore.js +42 -6
- package/dist/qaReview.js +38 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +9 -0
- package/dist/qaVerificationContract.js +76 -1
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +65 -9
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPopulation.js +2 -0
- package/dist/workflowDb.js +39 -5
- 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.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
|
5
5
|
import type { BranchPlan } from "../branch/types.js";
|
|
6
6
|
import { type DeliveryConfig, type DeliveryStack } from "../tickets/delivery.js";
|
|
7
7
|
import type { QaNonconvergenceContext, QaNonconvergenceDecision, QaReportRecoveryHandler } from "../qaReview.js";
|
|
8
|
-
import { RunObserver } from "../observability.js";
|
|
8
|
+
import { RunObserver, type ObservabilityMode } from "../observability.js";
|
|
9
9
|
export declare function formatStartResumeCommand(executable: "ai-foreman", projectDir: string, steps: number, sessionId: string): string;
|
|
10
10
|
export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman", projectDir: string, steps: number, sessionId?: string, run?: {
|
|
11
11
|
runId: string;
|
|
@@ -22,6 +22,7 @@ export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman",
|
|
|
22
22
|
export declare function savedStartArguments(command: Command, projectDir: string, opts: Record<string, unknown>): string[];
|
|
23
23
|
/** Pure validation shared by interactive start and persisted preparation. */
|
|
24
24
|
export declare function validateStartOptions(opts: Record<string, any>, requireCount: boolean): void;
|
|
25
|
+
export declare function resolveAuditLoggingMode(cli: unknown, configured: unknown, environment?: string | undefined): ObservabilityMode;
|
|
25
26
|
/** Preserve the distinction between a staged handoff and an unattempted acceptance. */
|
|
26
27
|
export declare function recordHandoffAcceptanceNotAttempted(projectDir: string, runId: string, role: "builder" | "qa", generation: number, stage: string, detail: string): void;
|
|
27
28
|
/** Default start selection is deliberately the same eligibility model users see in the ticket queue. */
|
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";
|
|
@@ -426,6 +426,20 @@ export function validateStartOptions(opts, requireCount) {
|
|
|
426
426
|
throw new Error("Preparation launch requires both run and token");
|
|
427
427
|
if (opts.launchToken && !opts.preparationRun && !opts.recoverRun)
|
|
428
428
|
throw new Error("Launch token requires a preparation or recovery run");
|
|
429
|
+
if (opts.auditLogging !== undefined && !["off", "summary", "trace"].includes(String(opts.auditLogging)))
|
|
430
|
+
throw new Error("--audit-logging must be off, summary, or trace");
|
|
431
|
+
}
|
|
432
|
+
export function resolveAuditLoggingMode(cli, configured, environment = process.env.RAFI_AUDIT_LOGGING) {
|
|
433
|
+
const value = cli ?? configured ?? environment ?? "summary";
|
|
434
|
+
if (!["off", "summary", "trace"].includes(String(value)))
|
|
435
|
+
throw new Error("audit logging must be off, summary, or trace");
|
|
436
|
+
return String(value);
|
|
437
|
+
}
|
|
438
|
+
function configuredAuditLoggingMode(path) {
|
|
439
|
+
if (!existsSync(path))
|
|
440
|
+
return undefined;
|
|
441
|
+
const raw = parseYaml(readFileSync(path, "utf8"));
|
|
442
|
+
return raw?.observability?.mode === undefined ? undefined : resolveAuditLoggingMode(undefined, raw.observability.mode, undefined);
|
|
429
443
|
}
|
|
430
444
|
/** A current waiver is an already-recorded decision: surface it, never demand a second start-time approval. */
|
|
431
445
|
function warnRiskPlanWaivers(projectDir, ticketIds) {
|
|
@@ -545,6 +559,7 @@ export function buildStartCommand(options = {}) {
|
|
|
545
559
|
.option("--base <ref>", "base ref for root ticket branches (default: current branch or HEAD)")
|
|
546
560
|
.option("--branch-prefix <prefix>", "branch name prefix for ticket branches")
|
|
547
561
|
.option("--show-session-cost", "show authoritative cost or trustworthy cumulative session tokens for Builder and QA")
|
|
562
|
+
.option("--audit-logging <mode>", "host-owned audit logging (off | summary | trace)", "summary")
|
|
548
563
|
.option("--hide-session-cost", "hide session cost/token usage for Builder and QA")
|
|
549
564
|
.option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
|
|
550
565
|
.option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
|
|
@@ -676,7 +691,20 @@ export function buildStartCommand(options = {}) {
|
|
|
676
691
|
const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
|
|
677
692
|
const autoApprovePlanUpdates = recoveryRecord
|
|
678
693
|
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
694
|
+
&& (recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-yes" || recoveryRecord.recoveryDecision.planUpdateApprovalSource === "explicit-choice")
|
|
679
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
|
+
}
|
|
680
708
|
if (opts.recoveryMode) {
|
|
681
709
|
const allowed = ["exact-session", "fresh-with-handoff", "fresh-recovery-only", "guided-recovery"];
|
|
682
710
|
if (!allowed.includes(String(opts.recoveryMode)))
|
|
@@ -934,6 +962,8 @@ export function buildStartCommand(options = {}) {
|
|
|
934
962
|
? recoveryExecutionTickets(recoveryRecord, requestedTicketIds, Boolean(opts.recoveryMode))
|
|
935
963
|
: selectedNewTicket ? [selectedNewTicket] : undefined;
|
|
936
964
|
const config = loadConfig(join(cwd, "foreman.yaml"));
|
|
965
|
+
const auditLogging = resolveAuditLoggingMode(command.getOptionValueSource("auditLogging") === "cli" ? opts.auditLogging : undefined, configuredAuditLoggingMode(join(cwd, "foreman.yaml")));
|
|
966
|
+
const observabilityConfig = { ...config.observability, mode: auditLogging, enabled: auditLogging !== "off" };
|
|
937
967
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
938
968
|
const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
|
|
939
969
|
const log = new Log(logPath);
|
|
@@ -948,9 +978,9 @@ export function buildStartCommand(options = {}) {
|
|
|
948
978
|
console.error(`rafi: warning: ${qaResolution.deprecationWarning}`);
|
|
949
979
|
const invocationRunId = supervisedInvocationId;
|
|
950
980
|
let observabilityStore;
|
|
951
|
-
if (
|
|
981
|
+
if (observabilityConfig.enabled) {
|
|
952
982
|
try {
|
|
953
|
-
observabilityStore = new ObservabilityStore(cwd, { config:
|
|
983
|
+
observabilityStore = new ObservabilityStore(cwd, { config: observabilityConfig });
|
|
954
984
|
}
|
|
955
985
|
catch (error) {
|
|
956
986
|
console.error(`rafi: observability unavailable; build will continue: ${String(error).replace(/\s+/g, " ").slice(0, 300)}`);
|
|
@@ -2384,7 +2414,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2384
2414
|
observabilityStore?.enforceLimits();
|
|
2385
2415
|
observabilityStore?.maintainLogs(cwd);
|
|
2386
2416
|
observabilityStore?.close();
|
|
2387
|
-
pruneRecoveryTelemetryAtCompletion(cwd,
|
|
2417
|
+
pruneRecoveryTelemetryAtCompletion(cwd, observabilityConfig.detail_retention_days);
|
|
2388
2418
|
activeObserver = undefined;
|
|
2389
2419
|
process.off("exit", finishObservationOnExit);
|
|
2390
2420
|
console.log("foreman: branch run summary");
|
|
@@ -2831,7 +2861,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2831
2861
|
observabilityStore?.enforceLimits();
|
|
2832
2862
|
observabilityStore?.maintainLogs(cwd);
|
|
2833
2863
|
observabilityStore?.close();
|
|
2834
|
-
pruneRecoveryTelemetryAtCompletion(cwd,
|
|
2864
|
+
pruneRecoveryTelemetryAtCompletion(cwd, observabilityConfig.detail_retention_days);
|
|
2835
2865
|
activeObserver = undefined;
|
|
2836
2866
|
process.off("exit", finishObservationOnExit);
|
|
2837
2867
|
console.log(`\n${accent("foreman:")} ${result.completed}/${result.requested} step(s) completed`);
|
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/config.js
CHANGED
|
@@ -84,9 +84,16 @@ export function loadConfig(path = "foreman.yaml") {
|
|
|
84
84
|
...(raw.notifications ?? {}),
|
|
85
85
|
},
|
|
86
86
|
qa: { ...DEFAULT_CONFIG.qa, ...(raw.qa ?? {}) },
|
|
87
|
-
observability: { ...DEFAULT_CONFIG.observability, ...(raw.observability ?? {}) },
|
|
87
|
+
observability: normalizeObservabilityConfig({ ...DEFAULT_CONFIG.observability, ...(raw.observability ?? {}) }),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
function normalizeObservabilityConfig(config) {
|
|
91
|
+
if (config.enabled === false)
|
|
92
|
+
return { ...config, mode: "off" };
|
|
93
|
+
if (config.mode === "off")
|
|
94
|
+
return { ...config, enabled: false };
|
|
95
|
+
return config;
|
|
96
|
+
}
|
|
90
97
|
function validateConfig(raw, path) {
|
|
91
98
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
92
99
|
throw new Error(`${path}: expected a YAML object`);
|
|
@@ -123,17 +130,19 @@ function validateObservability(value, path) {
|
|
|
123
130
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
124
131
|
throw new Error(`${path}: observability must be an object`);
|
|
125
132
|
const input = value;
|
|
126
|
-
const keys = ["enabled", "sample_interval_seconds", "detail_retention_days", "log_retention_days", "detail_soft_limit_mb", "detail_hard_limit_mb", "log_limit_mb"];
|
|
133
|
+
const keys = ["enabled", "mode", "sample_interval_seconds", "detail_retention_days", "log_retention_days", "detail_soft_limit_mb", "detail_hard_limit_mb", "log_limit_mb"];
|
|
127
134
|
for (const key of Object.keys(input))
|
|
128
135
|
if (!keys.includes(key))
|
|
129
136
|
throw new Error(`${path}: unknown observability setting ${key}`);
|
|
130
137
|
if (input.enabled !== undefined && typeof input.enabled !== "boolean")
|
|
131
138
|
throw new Error(`${path}: observability.enabled must be a boolean`);
|
|
132
|
-
|
|
139
|
+
if (input.mode !== undefined && !["off", "summary", "trace"].includes(String(input.mode)))
|
|
140
|
+
throw new Error(`${path}: observability.mode must be off, summary, or trace`);
|
|
141
|
+
for (const key of keys.filter((key) => key !== "enabled" && key !== "mode")) {
|
|
133
142
|
if (input[key] !== undefined && (!Number.isInteger(input[key]) || Number(input[key]) <= 0))
|
|
134
143
|
throw new Error(`${path}: observability.${key} must be a positive integer`);
|
|
135
144
|
}
|
|
136
|
-
const merged = { ...DEFAULT_OBSERVABILITY_CONFIG, ...input };
|
|
145
|
+
const merged = { ...DEFAULT_OBSERVABILITY_CONFIG, ...input, ...(input.enabled === false && input.mode === undefined ? { mode: "off" } : {}), ...(input.mode === "off" && input.enabled === undefined ? { enabled: false } : {}) };
|
|
137
146
|
if (merged.detail_soft_limit_mb >= merged.detail_hard_limit_mb)
|
|
138
147
|
throw new Error(`${path}: observability.detail_soft_limit_mb must be less than detail_hard_limit_mb`);
|
|
139
148
|
}
|
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;
|