ai-foreman 1.7.19 → 1.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
2
|
+
import { buildScopeRevision } from "./buildApproval.js";
|
|
3
|
+
import { loadTicketsConfig, resolveTicketPaths } from "./tickets/config.js";
|
|
4
|
+
import { loadTickets } from "./tickets/ticketLoader.js";
|
|
5
|
+
import { qaDigest } from "./qaProtocolV2.js";
|
|
6
|
+
/** Called by the CLI only after its source-bound approval gate succeeds. */
|
|
7
|
+
export function admitApprovedTicket(projectDir, runId, ticketId, approvedRevision, authorization) {
|
|
8
|
+
const ticket = loadTickets(resolveTicketPaths(loadTicketsConfig(projectDir), projectDir).tickets).find(ticket => ticket.id === ticketId);
|
|
9
|
+
if (!ticket || buildScopeRevision(projectDir) !== approvedRevision)
|
|
10
|
+
throw new Error("Ticket requirements changed after approval; reselect and approve current work");
|
|
11
|
+
const db = new WorkflowDb(projectDir);
|
|
12
|
+
try {
|
|
13
|
+
db.atomic(() => {
|
|
14
|
+
const current = loadTickets(resolveTicketPaths(loadTicketsConfig(projectDir), projectDir).tickets).find(ticket => ticket.id === ticketId);
|
|
15
|
+
if (!current || buildScopeRevision(projectDir) !== approvedRevision || JSON.stringify(current) !== JSON.stringify(ticket))
|
|
16
|
+
throw new Error("Ticket eligibility or requirements changed during admission; reselect approved work");
|
|
17
|
+
db.admitWork({ runId, kind: "ticket", ticketId, definition: ticket, approvalId: qaDigest("build-work-approval", { runId, approvedRevision, authorization }), scopeRevision: approvedRevision, provenance: { userTurn: authorization, reason: "CLI source-bound build approval gate succeeded" } });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
db.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Full authoritative definition, shared by initial/resumed/remediation paths. */
|
|
2
|
+
export function renderBuildWorkContext(ticket, checklist) {
|
|
3
|
+
return ["Full assigned work definition (scope authority):", JSON.stringify(ticket, null, 2),
|
|
4
|
+
"Project validation checklist:", ...(checklist.length ? checklist.map((item, index) => `${index + 1}. ${item}`) : ["No additional project checklist configured."]),
|
|
5
|
+
"Implement this assigned work and provide verification evidence. Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation."].join("\n");
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
export declare const BUILD_WORK_SCHEMA_VERSION = 4;
|
|
3
|
+
/** A copied database must be rebound by state transfer, never by an ordinary writer. */
|
|
4
|
+
export declare function assertBuildProject(db: Database.Database, projectDir: string): void;
|
|
5
|
+
/** Classify legacy records without converting observed progress into permission. */
|
|
6
|
+
export declare function migrateBuildWork(db: Database.Database, projectDir: string, faults?: {
|
|
7
|
+
beforeGuards?(): void;
|
|
8
|
+
beforeCommit?(): void;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, cpSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { classifyProcess } from "./processIdentity.js";
|
|
5
|
+
export const BUILD_WORK_SCHEMA_VERSION = 4;
|
|
6
|
+
const migration = "004_admitted_build_work";
|
|
7
|
+
/** A copied database must be rebound by state transfer, never by an ordinary writer. */
|
|
8
|
+
export function assertBuildProject(db, projectDir) {
|
|
9
|
+
const identity = db.prepare("SELECT canonical_root FROM build_project_identity WHERE singleton=1").get();
|
|
10
|
+
if (identity.canonical_root !== realpathSync(projectDir))
|
|
11
|
+
throw new Error("Build project identity requires explicit state-transfer rebinding");
|
|
12
|
+
}
|
|
13
|
+
/** Classify legacy records without converting observed progress into permission. */
|
|
14
|
+
export function migrateBuildWork(db, projectDir, faults) {
|
|
15
|
+
if (db.prepare("SELECT 1 FROM recovery_schema_migrations WHERE migration=?").get(migration)) {
|
|
16
|
+
assertBuildProject(db, projectDir);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const lease = db.prepare("SELECT pid,process_start,host FROM project_lease").get();
|
|
20
|
+
if (lease && classifyProcess(lease.pid, lease.process_start, lease.host).state !== "dead")
|
|
21
|
+
throw new Error("Work admission upgrade requires stopped, verified writers and a consistent backup");
|
|
22
|
+
const owner = db.prepare("SELECT record_json FROM build_admission").get();
|
|
23
|
+
if (owner) {
|
|
24
|
+
const identity = JSON.parse(owner.record_json);
|
|
25
|
+
if (classifyProcess(identity.pid, identity.processStart, identity.host).state !== "dead")
|
|
26
|
+
throw new Error("Work admission upgrade requires a stopped, verified build owner");
|
|
27
|
+
}
|
|
28
|
+
const backup = db.serialize();
|
|
29
|
+
// The coherent WAL-visible backup survives even a rolled-back upgrade.
|
|
30
|
+
// Publish the packet copy and manifest before enabling any new writer.
|
|
31
|
+
const backupDigest = createHash("sha256").update(backup).digest("hex");
|
|
32
|
+
const backupRoot = join(projectDir, ".rafi", "backups", "work-admission-v4");
|
|
33
|
+
const backupPath = join(backupRoot, backupDigest);
|
|
34
|
+
mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
35
|
+
if (!existsSync(backupPath)) {
|
|
36
|
+
const staging = join(backupRoot, `${backupDigest}.${randomUUID()}.tmp`);
|
|
37
|
+
mkdirSync(staging, { mode: 0o700 });
|
|
38
|
+
const file = join(staging, "recovery.sqlite3");
|
|
39
|
+
writeFileSync(file, backup, { mode: 0o600 });
|
|
40
|
+
const descriptor = openSync(file, "r");
|
|
41
|
+
try {
|
|
42
|
+
fsyncSync(descriptor);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
closeSync(descriptor);
|
|
46
|
+
}
|
|
47
|
+
const packets = join(projectDir, ".foreman", "qa-report-recovery");
|
|
48
|
+
if (existsSync(packets))
|
|
49
|
+
cpSync(packets, join(staging, "qa-report-recovery"), { recursive: true, dereference: false });
|
|
50
|
+
const packetFiles = syncBackupTree(staging);
|
|
51
|
+
const manifest = join(staging, "manifest.json");
|
|
52
|
+
writeFileSync(manifest, JSON.stringify({ version: 1, databaseDigest: backupDigest, canonicalRoot: realpathSync(projectDir), createdAt: new Date().toISOString(), packetCopy: existsSync(packets), files: packetFiles }), { mode: 0o600 });
|
|
53
|
+
syncFile(manifest);
|
|
54
|
+
syncDirectory(staging);
|
|
55
|
+
renameSync(staging, backupPath);
|
|
56
|
+
syncDirectory(backupRoot);
|
|
57
|
+
}
|
|
58
|
+
if (createHash("sha256").update(readFileSync(join(backupPath, "recovery.sqlite3"))).digest("hex") !== backupDigest)
|
|
59
|
+
throw new Error("Work admission backup failed integrity verification; preserve state before retry");
|
|
60
|
+
try {
|
|
61
|
+
db.transaction(() => {
|
|
62
|
+
db.exec(`
|
|
63
|
+
CREATE TABLE build_work_upgrade_backup(singleton INTEGER PRIMARY KEY CHECK(singleton=1),database_bytes BLOB NOT NULL,created_at TEXT NOT NULL);
|
|
64
|
+
CREATE TABLE build_project_identity(singleton INTEGER PRIMARY KEY CHECK(singleton=1),project_id TEXT NOT NULL UNIQUE,canonical_root TEXT NOT NULL);
|
|
65
|
+
CREATE TABLE build_work_scope(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,kind TEXT NOT NULL CHECK(kind IN ('ticket','synthetic')),ticket_id TEXT,state TEXT NOT NULL CHECK(state IN ('admitted','quarantined')),definition_json TEXT NOT NULL,PRIMARY KEY(run_id,work_id),UNIQUE(run_id,ticket_id),CHECK((kind='ticket' AND ticket_id=work_id) OR (kind='synthetic' AND ticket_id IS NULL)));
|
|
66
|
+
CREATE TABLE build_work_admissions(sequence INTEGER PRIMARY KEY AUTOINCREMENT,admission_id TEXT NOT NULL UNIQUE,run_id TEXT NOT NULL,work_id TEXT NOT NULL,assignment_id TEXT NOT NULL,approval_id TEXT NOT NULL,scope_revision TEXT NOT NULL,requirements_digest TEXT NOT NULL,record_json TEXT NOT NULL,admitted_at TEXT NOT NULL,FOREIGN KEY(run_id,work_id) REFERENCES build_work_scope(run_id,work_id));
|
|
67
|
+
CREATE TABLE build_assignments(assignment_id TEXT PRIMARY KEY,run_id TEXT NOT NULL,work_id TEXT NOT NULL,operation_id TEXT NOT NULL UNIQUE,record_json TEXT NOT NULL,FOREIGN KEY(run_id,work_id) REFERENCES build_work_scope(run_id,work_id));
|
|
68
|
+
CREATE TABLE build_ownership_conflicts(conflict_id TEXT PRIMARY KEY,run_id TEXT NOT NULL,work_id TEXT NOT NULL,status TEXT NOT NULL,revision INTEGER NOT NULL,record_json TEXT NOT NULL,FOREIGN KEY(run_id,work_id) REFERENCES build_work_scope(run_id,work_id));
|
|
69
|
+
CREATE TABLE build_work_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,table_name TEXT NOT NULL,record_key TEXT NOT NULL,action TEXT NOT NULL,record_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
70
|
+
CREATE TABLE build_reconciliations(reconciliation_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),expected_revision TEXT NOT NULL,record_json TEXT NOT NULL,created_at TEXT NOT NULL);
|
|
71
|
+
`);
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
db.prepare("INSERT INTO build_work_upgrade_backup VALUES(1,?,?)").run(backup, now);
|
|
74
|
+
db.prepare("INSERT INTO build_project_identity VALUES(1,?,?)").run(randomUUID(), realpathSync(projectDir));
|
|
75
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'qa_%'").all().filter(({ name }) => /^[a-z_]+$/.test(name));
|
|
76
|
+
for (const { name } of tables) {
|
|
77
|
+
const columns = db.prepare(`PRAGMA table_info(${name})`).all();
|
|
78
|
+
if (!columns.some(column => column.name === "run_id") || !columns.some(column => column.name === "ticket_id"))
|
|
79
|
+
continue;
|
|
80
|
+
for (const row of db.prepare(`SELECT DISTINCT run_id,ticket_id FROM ${name}`).all()) {
|
|
81
|
+
db.prepare("INSERT OR IGNORE INTO build_work_scope VALUES(?,?,'ticket',?,'quarantined','{}')").run(row.run_id, row.ticket_id, row.ticket_id);
|
|
82
|
+
db.prepare("INSERT OR IGNORE INTO build_ownership_conflicts VALUES(?,?,?,'unresolved',1,?)").run(`legacy:${row.run_id}:${row.ticket_id}`, row.run_id, row.ticket_id, JSON.stringify({ reason: "Legacy QA progress is not proof of run-bound approval and assignment", classification: "ownership_unestablished", observedTable: name }));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
faults?.beforeGuards?.();
|
|
86
|
+
// Replace V3 guards atomically. Old connections register 3 and fail even on heartbeat writes.
|
|
87
|
+
for (const trigger of db.prepare("SELECT name,sql FROM sqlite_master WHERE type='trigger' AND name LIKE 'handback_v3_%'").all()) {
|
|
88
|
+
db.exec(`DROP TRIGGER "${trigger.name}"`);
|
|
89
|
+
db.exec(trigger.sql.replace("rafi_writer_protocol() != 3", "rafi_writer_protocol() != 4"));
|
|
90
|
+
}
|
|
91
|
+
db.function("rafi_writer_protocol", () => 4);
|
|
92
|
+
for (const { name } of db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'build_%'").all()) {
|
|
93
|
+
if (!/^[a-z_]+$/.test(name))
|
|
94
|
+
throw new Error("Unexpected build storage table");
|
|
95
|
+
for (const action of ["INSERT", "UPDATE", "DELETE"])
|
|
96
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS work_v4_protocol_${name}_${action} BEFORE ${action} ON ${name} BEGIN SELECT CASE WHEN rafi_writer_protocol()<>4 THEN RAISE(ABORT,'incompatible work admission writer') END; END`);
|
|
97
|
+
}
|
|
98
|
+
for (const { name } of tables) {
|
|
99
|
+
const columns = db.prepare(`PRAGMA table_info(${name})`).all();
|
|
100
|
+
let scope;
|
|
101
|
+
if (columns.some(column => column.name === "run_id") && columns.some(column => column.name === "ticket_id"))
|
|
102
|
+
scope = target => `s.run_id=${target}.run_id AND s.work_id=${target}.ticket_id`;
|
|
103
|
+
else if (columns.some(column => column.name === "run_id") && columns.some(column => column.name === "work_id"))
|
|
104
|
+
scope = target => `s.run_id=${target}.run_id AND s.work_id=${target}.work_id`;
|
|
105
|
+
else if (name === "qa_preparation_attempts" || name === "qa_preparation_progress")
|
|
106
|
+
scope = target => `EXISTS(SELECT 1 FROM qa_preparation_budgets b WHERE b.id=${target}.budget_id AND b.run_id=s.run_id AND b.work_id=s.work_id)`;
|
|
107
|
+
else if (name === "qa_contract_receipts")
|
|
108
|
+
scope = target => `EXISTS(SELECT 1 FROM qa_verification_contracts c WHERE c.digest=${target}.contract_digest AND c.run_id=s.run_id AND c.work_id=s.work_id)`;
|
|
109
|
+
else if (columns.some(column => column.name === "report_occurrence_id"))
|
|
110
|
+
scope = target => `EXISTS(SELECT 1 FROM qa_reports r WHERE r.report_occurrence_id=${target}.report_occurrence_id AND r.run_id=s.run_id AND r.ticket_id=s.work_id)`;
|
|
111
|
+
else if (columns.some(column => column.name === "operation_id"))
|
|
112
|
+
scope = target => `EXISTS(SELECT 1 FROM qa_turns t WHERE t.operation_id=${target}.operation_id AND t.run_id=s.run_id AND t.ticket_id=s.work_id)`;
|
|
113
|
+
else
|
|
114
|
+
continue;
|
|
115
|
+
for (const action of ["INSERT", "UPDATE", "DELETE"]) {
|
|
116
|
+
const target = action === "DELETE" ? "OLD" : "NEW";
|
|
117
|
+
db.exec(`CREATE TRIGGER work_v4_scope_${name}_${action} BEFORE ${action} ON ${name} WHEN NOT EXISTS(SELECT 1 FROM build_work_scope s WHERE ${scope(target)} AND s.state='admitted') BEGIN SELECT RAISE(ABORT,'QA work is not admitted; reconcile ownership first'); END`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const { name } of tables) {
|
|
121
|
+
const columns = db.prepare(`PRAGMA table_info(${name})`).all();
|
|
122
|
+
if (!columns.some(column => column.name === "run_id") || !columns.some(column => column.name === "ticket_id"))
|
|
123
|
+
continue;
|
|
124
|
+
const keys = columns.filter(column => column.pk).sort((a, b) => a.pk - b.pk).map(column => `NEW.${column.name}`);
|
|
125
|
+
const retained = columns.filter(column => column.pk || ["status", "state", "disposition", "review_number", "from_revision", "to_revision", "source_digest", "source_state_digest", "report_digest", "review_basis_digest", "certificate_id", "generation", "request_digest", "response_digest", "summary_digest", "event_json"].includes(column.name)).flatMap(column => [`'${column.name}'`, `NEW.${column.name}`]);
|
|
126
|
+
for (const action of ["INSERT", "UPDATE"])
|
|
127
|
+
db.exec(`CREATE TRIGGER work_v4_order_${name}_${action} AFTER ${action} ON ${name} BEGIN INSERT INTO build_work_events(run_id,work_id,table_name,record_key,action,record_json,created_at) VALUES(NEW.run_id,NEW.ticket_id,'${name}',json_array(${keys.join(",")}), '${action.toLowerCase()}',json_object(${retained.join(",")}),strftime('%Y-%m-%dT%H:%M:%fZ','now')); END`);
|
|
128
|
+
}
|
|
129
|
+
for (const { name } of tables) {
|
|
130
|
+
const columns = db.prepare(`PRAGMA table_info(${name})`).all();
|
|
131
|
+
if (columns.some(column => column.name === "run_id") && columns.some(column => column.name === "ticket_id"))
|
|
132
|
+
db.exec(`CREATE TRIGGER work_v4_identity_${name} BEFORE UPDATE ON ${name} WHEN NEW.run_id<>OLD.run_id OR NEW.ticket_id<>OLD.ticket_id BEGIN SELECT RAISE(ABORT,'Scoped QA identity is immutable'); END`);
|
|
133
|
+
}
|
|
134
|
+
for (const action of ["INSERT", "UPDATE"]) {
|
|
135
|
+
db.exec(`CREATE TRIGGER work_v4_report_parent_${action} BEFORE ${action} ON qa_reports WHEN NOT EXISTS(SELECT 1 FROM qa_review_attempts p WHERE p.run_id=NEW.run_id AND p.ticket_id=NEW.ticket_id AND p.review_number=NEW.review_number AND p.source_digest=NEW.source_state_digest AND p.report_digest=NEW.report_digest) BEGIN SELECT RAISE(ABORT,'QA report parent ownership mismatch'); END;
|
|
136
|
+
CREATE TRIGGER work_v4_chain_parent_${action} BEFORE ${action} ON qa_report_chains WHEN NOT EXISTS(SELECT 1 FROM qa_reports p JOIN qa_reports s ON p.run_id=s.run_id AND p.ticket_id=s.ticket_id WHERE p.report_occurrence_id=NEW.predecessor_occurrence_id AND s.report_occurrence_id=NEW.successor_occurrence_id AND p.report_digest=NEW.predecessor_report_digest AND s.report_digest=NEW.successor_report_digest) BEGIN SELECT RAISE(ABORT,'QA report chain parent ownership mismatch'); END;
|
|
137
|
+
CREATE TRIGGER work_v4_assignment_${action} BEFORE ${action} ON operation_journal WHEN NEW.kind='build-assignment' AND NOT EXISTS(SELECT 1 FROM build_work_scope s WHERE s.run_id=NEW.run_id AND s.work_id=json_extract(NEW.intent_json,'$.ticketId') AND s.state='admitted') BEGIN SELECT RAISE(ABORT,'Builder assignment is not admitted'); END;
|
|
138
|
+
CREATE TRIGGER work_v4_branch_${action} BEFORE ${action} ON branch_resume_sessions WHEN NOT EXISTS(SELECT 1 FROM build_work_scope s WHERE s.run_id=NEW.run_id AND s.work_id=NEW.ticket AND s.state='admitted') BEGIN SELECT RAISE(ABORT,'Branch work is not admitted'); END;`);
|
|
139
|
+
}
|
|
140
|
+
db.exec(`CREATE TRIGGER work_v4_membership_update BEFORE UPDATE OF state_json ON workflow_runs WHEN EXISTS(SELECT 1 FROM build_work_admissions a WHERE a.run_id=NEW.run_id AND NOT EXISTS(SELECT 1 FROM json_each(NEW.state_json,'$.tickets') j WHERE j.value=a.work_id)) AND json_extract(NEW.state_json,'$.version') IS NOT NULL BEGIN SELECT RAISE(ABORT,'Build snapshot cannot drop admitted work'); END;
|
|
141
|
+
CREATE TRIGGER work_v4_admission_update BEFORE UPDATE ON build_work_admissions BEGIN SELECT RAISE(ABORT,'Work admissions are immutable'); END;
|
|
142
|
+
CREATE TRIGGER work_v4_admission_delete BEFORE DELETE ON build_work_admissions BEGIN SELECT RAISE(ABORT,'Work admissions are immutable'); END;
|
|
143
|
+
CREATE TRIGGER work_v4_scope_identity BEFORE UPDATE ON build_work_scope WHEN NEW.run_id<>OLD.run_id OR NEW.work_id<>OLD.work_id OR NEW.kind<>OLD.kind OR NEW.ticket_id IS NOT OLD.ticket_id BEGIN SELECT RAISE(ABORT,'Work identity is immutable'); END;
|
|
144
|
+
CREATE TRIGGER work_v4_scope_delete BEFORE DELETE ON build_work_scope BEGIN SELECT RAISE(ABORT,'Work scope must be retained'); END;`);
|
|
145
|
+
for (const table of ["build_work_scope", "build_work_admissions", "build_assignments", "build_ownership_conflicts", "build_reconciliations"]) {
|
|
146
|
+
for (const action of ["INSERT", "UPDATE", "DELETE"])
|
|
147
|
+
db.exec(`CREATE TRIGGER work_v4_authority_${table}_${action} BEFORE ${action} ON ${table} WHEN rafi_work_authority()=0 BEGIN SELECT RAISE(ABORT,'Restricted work admission authority required'); END`);
|
|
148
|
+
}
|
|
149
|
+
for (const action of ["INSERT", "UPDATE"])
|
|
150
|
+
db.exec(`CREATE TRIGGER work_v4_remediation_parent_${action} BEFORE ${action} ON qa_remediation_attempts WHEN NOT EXISTS(SELECT 1 FROM qa_review_attempts p WHERE p.attempt_id=NEW.review_attempt_id AND p.run_id=NEW.run_id AND p.ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'Remediation parent ownership mismatch'); END;
|
|
151
|
+
CREATE TRIGGER work_v4_finalization_parent_${action} BEFORE ${action} ON qa_finalization_steps WHEN NOT EXISTS(SELECT 1 FROM qa_pass_certificates p WHERE p.certificate_id=NEW.certificate_id AND p.run_id=NEW.run_id AND p.ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'Finalization parent ownership mismatch'); END;
|
|
152
|
+
CREATE TRIGGER work_v4_handoff_parent_${action} BEFORE ${action} ON qa_failure_handoffs WHEN NOT EXISTS(SELECT 1 FROM qa_reports p JOIN qa_review_attempts a ON a.run_id=p.run_id AND a.ticket_id=p.ticket_id AND a.review_number=p.review_number WHERE p.report_occurrence_id=NEW.report_occurrence_id AND p.run_id=NEW.run_id AND p.ticket_id=NEW.ticket_id AND p.report_digest=NEW.report_digest AND a.attempt_id=NEW.review_attempt_id) BEGIN SELECT RAISE(ABORT,'Failure handoff parent ownership mismatch'); END;
|
|
153
|
+
CREATE TRIGGER work_v4_authorization_parent_${action} BEFORE ${action} ON qa_remediation_authorizations WHEN NOT EXISTS(SELECT 1 FROM qa_review_attempts p WHERE p.attempt_id=NEW.review_attempt_id AND p.run_id=NEW.run_id AND p.ticket_id=NEW.ticket_id) BEGIN SELECT RAISE(ABORT,'Authorization parent ownership mismatch'); END;`);
|
|
154
|
+
for (const action of ["INSERT", "UPDATE"]) {
|
|
155
|
+
db.exec(`CREATE TRIGGER work_v4_delivery_parent_${action} BEFORE ${action} ON qa_delivery_turns WHEN NOT EXISTS(SELECT 1 FROM qa_failure_handoffs h WHERE h.operation_id=NEW.operation_id AND h.report_occurrence_id=NEW.report_occurrence_id) BEGIN SELECT RAISE(ABORT,'QA delivery parent ownership mismatch'); END`);
|
|
156
|
+
for (const table of ["qa_findings", "qa_report_dispositions"])
|
|
157
|
+
db.exec(`CREATE TRIGGER work_v4_occurrence_digest_${table}_${action} BEFORE ${action} ON ${table} WHEN NOT EXISTS(SELECT 1 FROM qa_reports r WHERE r.report_occurrence_id=NEW.report_occurrence_id AND r.report_digest=NEW.report_digest) BEGIN SELECT RAISE(ABORT,'QA occurrence digest ownership mismatch'); END`);
|
|
158
|
+
}
|
|
159
|
+
db.exec(`CREATE TRIGGER work_v4_delivery_identity BEFORE UPDATE ON qa_delivery_turns WHEN NEW.operation_id<>OLD.operation_id OR NEW.report_occurrence_id<>OLD.report_occurrence_id OR NEW.turn_index<>OLD.turn_index OR NEW.turn_record_id<>OLD.turn_record_id BEGIN SELECT RAISE(ABORT,'QA delivery identity is immutable'); END`);
|
|
160
|
+
for (const action of ["INSERT", "UPDATE"])
|
|
161
|
+
db.exec(`CREATE TRIGGER work_v4_certificate_parent_${action} BEFORE ${action} ON qa_pass_certificates WHEN NOT EXISTS(SELECT 1 FROM qa_turns t JOIN qa_turn_events e ON e.operation_id=t.operation_id AND e.event_index=0 WHERE t.run_id=NEW.run_id AND t.ticket_id=NEW.ticket_id AND t.source_state_digest=NEW.source_state_digest AND t.review_basis_digest=NEW.review_basis_digest AND t.status='completed' AND e.event_digest=NEW.turn_receipt_digest AND e.event_json=t.receipt_json AND json_extract(t.receipt_json,'$.terminalEventObserved')=1 AND length(json_extract(t.receipt_json,'$.providerTurnId'))>0) BEGIN SELECT RAISE(ABORT,'QA certificate completion receipt ownership mismatch'); END`);
|
|
162
|
+
for (const table of ["build_reconciliations", "build_work_events"])
|
|
163
|
+
for (const action of ["UPDATE", "DELETE"])
|
|
164
|
+
db.exec(`CREATE TRIGGER work_v4_immutable_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT RAISE(ABORT,'Build audit history is immutable'); END`);
|
|
165
|
+
faults?.beforeCommit?.();
|
|
166
|
+
if (db.pragma("foreign_key_check").length || db.pragma("integrity_check", { simple: true }) !== "ok")
|
|
167
|
+
throw new Error("Work admission upgrade integrity failure");
|
|
168
|
+
db.prepare("INSERT INTO recovery_schema_migrations VALUES(?,?)").run(migration, now);
|
|
169
|
+
db.pragma("user_version = 4");
|
|
170
|
+
}).immediate();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
db.function("rafi_writer_protocol", () => 3);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function syncFile(path) { const descriptor = openSync(path, "r"); try {
|
|
178
|
+
fsyncSync(descriptor);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
closeSync(descriptor);
|
|
182
|
+
} }
|
|
183
|
+
function syncDirectory(path) { if (process.platform !== "win32")
|
|
184
|
+
syncFile(path); }
|
|
185
|
+
function syncBackupTree(root) {
|
|
186
|
+
const files = [];
|
|
187
|
+
const visit = (directory) => {
|
|
188
|
+
chmodSync(directory, 0o700);
|
|
189
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
190
|
+
const path = join(directory, entry.name);
|
|
191
|
+
if (entry.isDirectory())
|
|
192
|
+
visit(path);
|
|
193
|
+
else {
|
|
194
|
+
const bytes = entry.isSymbolicLink() ? Buffer.from(readlinkSync(path)) : readFileSync(path);
|
|
195
|
+
if (!entry.isSymbolicLink()) {
|
|
196
|
+
chmodSync(path, 0o600);
|
|
197
|
+
syncFile(path);
|
|
198
|
+
}
|
|
199
|
+
files.push({ path: relative(root, path), kind: entry.isSymbolicLink() ? "symlink" : "file", digest: createHash("sha256").update(bytes).digest("hex") });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
syncDirectory(directory);
|
|
203
|
+
};
|
|
204
|
+
visit(root);
|
|
205
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
206
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { type BuildOwnershipRepairV1 } from "rafi-spec";
|
|
3
|
+
export type { BuildOwnershipRepairV1 } from "rafi-spec";
|
|
4
|
+
export declare function validateOwnershipRepair(value: unknown): asserts value is BuildOwnershipRepairV1;
|
|
5
|
+
/** Audited metadata repair; source separation/restoration must be inspected before this commit. */
|
|
6
|
+
export declare function reconcileWork(db: Database.Database, request: BuildOwnershipRepairV1, input: {
|
|
7
|
+
revision: string;
|
|
8
|
+
sourceDigest: string;
|
|
9
|
+
definition: unknown;
|
|
10
|
+
requiredPaths: string[];
|
|
11
|
+
userTurn: string;
|
|
12
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { admitWork } from "./buildWorkAdmission.js";
|
|
3
|
+
import { qaDigest } from "./qaProtocolV2.js";
|
|
4
|
+
import { validateBuildOwnershipRepairV1 } from "rafi-spec";
|
|
5
|
+
export function validateOwnershipRepair(value) {
|
|
6
|
+
const validation = validateBuildOwnershipRepairV1(value);
|
|
7
|
+
if (!validation.valid)
|
|
8
|
+
throw new Error(`Invalid ownership repair: ${validation.errors.join("; ")}`);
|
|
9
|
+
const request = value;
|
|
10
|
+
if ([request.authorization, request.attestation].some(text => !text.trim() || Buffer.byteLength(text) > 16384))
|
|
11
|
+
throw new Error("Repair requires bounded authorization and attestation");
|
|
12
|
+
if (new Set(request.mapping.map(item => item.path)).size !== request.mapping.length)
|
|
13
|
+
throw new Error("Repair mapping paths must be unique");
|
|
14
|
+
}
|
|
15
|
+
/** Audited metadata repair; source separation/restoration must be inspected before this commit. */
|
|
16
|
+
export function reconcileWork(db, request, input) {
|
|
17
|
+
const prior = db.prepare("SELECT record_json FROM build_reconciliations WHERE reconciliation_id=?").get(request.requestId);
|
|
18
|
+
if (prior) {
|
|
19
|
+
const receipt = JSON.parse(prior.record_json);
|
|
20
|
+
if (receipt.requestDigest !== qaDigest("ownership-repair-request", request))
|
|
21
|
+
throw new Error("Repair idempotency key collision");
|
|
22
|
+
return receipt;
|
|
23
|
+
}
|
|
24
|
+
if (request.expectedRevision !== input.revision || request.inspectedSourceDigest !== input.sourceDigest)
|
|
25
|
+
throw new Error("Ownership or inspected source changed; inspect the current revision before repair");
|
|
26
|
+
if (input.requiredPaths.some(path => !request.mapping.some(item => item.path === path && item.workId === request.workId)))
|
|
27
|
+
throw new Error("Every preserved change must have an inspected mapping to the authorized target work");
|
|
28
|
+
if (request.mapping.some(item => item.workId !== request.workId || !input.requiredPaths.includes(item.path)))
|
|
29
|
+
throw new Error("Mapping contains a foreign work identity or an uninspected path");
|
|
30
|
+
const uncertain = db.prepare("SELECT 1 FROM operation_journal WHERE run_id=? AND kind='provider-dispatch' AND status IN ('in_progress','uncertain')").get(request.runId);
|
|
31
|
+
if (uncertain)
|
|
32
|
+
throw new Error("Provider dispatch must be reconciled separately before ownership repair; attestation cannot establish nonexecution");
|
|
33
|
+
if (request.choice === "link_provenance") {
|
|
34
|
+
const decision = request.decisionId ? db.prepare("SELECT * FROM human_decisions WHERE decision_id=? AND run_id=? AND status='answered'").get(request.decisionId, request.runId) : undefined;
|
|
35
|
+
const approval = decision ? JSON.parse(decision.decision_json) : undefined;
|
|
36
|
+
const assignments = db.prepare("SELECT intent_json,result_json FROM operation_journal WHERE run_id=? AND kind='build-assignment' AND json_extract(intent_json,'$.ticketId')=? AND status='confirmed'").all(request.runId, request.workId);
|
|
37
|
+
const assignment = assignments.some(row => {
|
|
38
|
+
const intent = JSON.parse(row.intent_json), result = JSON.parse(row.result_json), session = result.providerMetadata?.sessionRef;
|
|
39
|
+
if (result.rejection || result.sourceError || !result.turnId || !result.responseDigest || !result.after?.digest || !intent.before?.digest || intent.requirementsDigest !== qaDigest("admitted-requirements", input.definition) || !session || session.source !== "observed" || !session.validatedAt || session.role !== "builder" || session.ticketId !== request.workId)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
if (realpathSync(session.cwd) !== realpathSync(intent.worktree))
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return !!db.prepare("SELECT 1 FROM provider_sessions WHERE run_id=? AND role='builder' AND session_id=? AND session_ref_json=?").get(request.runId, session.sessionId, JSON.stringify(session));
|
|
49
|
+
});
|
|
50
|
+
const checkpoint = db.prepare("SELECT 1 FROM workflow_events WHERE run_id=? AND (json_extract(payload_json,'$.ticketId')=? OR json_extract(payload_json,'$.currentTicket')=?)").get(request.runId, request.workId, request.workId);
|
|
51
|
+
if (!decision?.decision_key.includes(request.scopeRevision) || approval?.selectedChoiceId !== "proceed" || !assignment || !checkpoint)
|
|
52
|
+
throw new Error("Approval, assignment and checkpoint provenance is incomplete; use explicit mapped-work authorization instead");
|
|
53
|
+
}
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
const state = db.prepare("SELECT state FROM qa_ticket_heads WHERE run_id=? AND ticket_id=?").get(request.runId, request.workId)?.state;
|
|
56
|
+
const requiresFreshQa = request.choice !== "quarantine" && state !== "completed" && state !== "waived";
|
|
57
|
+
const receipt = { version: 1, reconciliationId: request.requestId, runId: request.runId, workId: request.workId, requestDigest: qaDigest("ownership-repair-request", request), request, userTurn: input.userTurn, beforeRevision: input.revision, sourceDigest: input.sourceDigest, historicalAuthorityProven: request.choice === "link_provenance", requiresFreshQa, executionDisposition: request.choice === "quarantine" ? "quarantined" : state === "completed" ? "terminal_metadata_repaired" : state === "waived" ? "existing_waiver_policy" : "fresh_qa_required", executionReopened: false, providerReplayAuthorized: false, createdAt: now };
|
|
58
|
+
db.prepare("INSERT INTO build_reconciliations VALUES(?,?,?,?,?)").run(request.requestId, request.runId, request.expectedRevision, JSON.stringify(receipt), now);
|
|
59
|
+
if (request.choice === "quarantine") {
|
|
60
|
+
db.prepare("UPDATE build_work_scope SET state='quarantined' WHERE run_id=? AND work_id=?").run(request.runId, request.workId);
|
|
61
|
+
db.prepare("UPDATE build_ownership_conflicts SET record_json=json_set(record_json,'$.manualDecision',?),revision=revision+1 WHERE run_id=? AND work_id=? AND status='unresolved'").run(request.requestId, request.runId, request.workId);
|
|
62
|
+
return receipt;
|
|
63
|
+
}
|
|
64
|
+
db.prepare("UPDATE build_work_scope SET state='admitted' WHERE run_id=? AND work_id=?").run(request.runId, request.workId);
|
|
65
|
+
admitWork(db, { runId: request.runId, kind: "ticket", ticketId: request.workId, definition: input.definition, approvalId: request.requestId, scopeRevision: request.scopeRevision, provenance: { userTurn: input.userTurn, decisionId: request.decisionId, reason: request.choice === "link_provenance" ? "Audited run-bound authorization provenance" : "Operator explicitly authorized inspected preserved changes for fresh verification" } }, true);
|
|
66
|
+
db.prepare("UPDATE build_ownership_conflicts SET status='reconciled',revision=revision+1,record_json=json_set(record_json,'$.reconciliationId',?) WHERE run_id=? AND work_id=? AND status='unresolved'").run(request.requestId, request.runId, request.workId);
|
|
67
|
+
// Retain every old byte and outcome. The audit permits fresh QA, never replay of this turn.
|
|
68
|
+
db.prepare("UPDATE operation_journal SET result_json=json_set(COALESCE(result_json,'{}'),'$.reconciliationId',?,'$.replayForbidden',1),updated_at=? WHERE run_id=? AND kind='build-assignment' AND json_extract(intent_json,'$.ticketId')=?").run(request.requestId, now, request.runId, request.workId);
|
|
69
|
+
return receipt;
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BuilderAdapter } from "./adapters/types.js";
|
|
2
|
+
import { type BuilderGuidanceFollowup } from "./buildAssignment.js";
|
|
3
|
+
import type { QaFixResult } from "./qaReview.js";
|
|
4
|
+
/** One journaled mutation shared by current-branch and isolated-worktree resume. */
|
|
5
|
+
export declare function deliverBuilderGuidanceFollowup(scope: {
|
|
6
|
+
projectDir: string;
|
|
7
|
+
runId: string;
|
|
8
|
+
ticketId: string;
|
|
9
|
+
worktree: string;
|
|
10
|
+
}, instruction: string, followup: BuilderGuidanceFollowup, host: {
|
|
11
|
+
prepare(instruction: string): Promise<BuilderAdapter>;
|
|
12
|
+
validateRequirements(): void;
|
|
13
|
+
recordSession?(adapter: BuilderAdapter): void;
|
|
14
|
+
completed?(operationId: string, adapter: BuilderAdapter): Promise<void>;
|
|
15
|
+
}): Promise<QaFixResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { collectBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
2
|
+
import { maintainGraphTask } from "./graph/boundary.js";
|
|
3
|
+
import { ensureBuilderContract } from "./qaBuildGate.js";
|
|
4
|
+
import { graphContext, sendGraphTurn } from "./graph/turn.js";
|
|
5
|
+
import { beginBuildAssignment, finishBuildAssignment } from "./buildAssignment.js";
|
|
6
|
+
import { parseStepStatus } from "./foreman.js";
|
|
7
|
+
/** One journaled mutation shared by current-branch and isolated-worktree resume. */
|
|
8
|
+
export async function deliverBuilderGuidanceFollowup(scope, instruction, followup, host) {
|
|
9
|
+
try {
|
|
10
|
+
const adapter = await host.prepare(instruction);
|
|
11
|
+
await adapter.prepareSession?.();
|
|
12
|
+
host.validateRequirements();
|
|
13
|
+
host.recordSession?.(adapter);
|
|
14
|
+
instruction += "\n" + await ensureBuilderContract(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter);
|
|
15
|
+
const assignment = beginBuildAssignment(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, instruction, followup, adapter);
|
|
16
|
+
// The handback policy prevents provider wrappers from retrying this mutation.
|
|
17
|
+
const result = await sendGraphTurn(adapter, assignment.instruction, graphContext(scope.projectDir, scope.worktree, "manager-guidance", assignment.instruction, { logicalTaskId: assignment.operationId, operationId: assignment.operationId, runId: scope.runId, workId: scope.ticketId }), { handback: true, logicalActionId: assignment.operationId });
|
|
18
|
+
const status = parseStepStatus(result.text);
|
|
19
|
+
if (!result.isError && !result.failure && status.kind === "done")
|
|
20
|
+
await collectBuilderContractCoverage(scope.projectDir, scope.runId, scope.ticketId, scope.worktree, adapter, assignment.operationId, assignment.instruction, result);
|
|
21
|
+
const rejection = finishBuildAssignment(scope.projectDir, assignment, result, status);
|
|
22
|
+
if (!rejection && !result.isError && status.kind === "done")
|
|
23
|
+
await maintainGraphTask(scope.projectDir, scope.worktree, assignment.operationId, adapter).catch(() => undefined);
|
|
24
|
+
await host.completed?.(assignment.operationId, adapter);
|
|
25
|
+
if (rejection || result.isError || result.failure || status.kind !== "done" || !result.turnId)
|
|
26
|
+
return { ok: false, detail: rejection ?? result.failure?.diagnostics ?? `Builder follow-up stopped with ${status.kind}; reconcile its result before continuing`, response: result.text };
|
|
27
|
+
return { ok: true, response: result.text, summary: status.summary ?? "Builder applied waiting guidance", providerTurnId: result.turnId };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// A thrown submission leaves the assignment in progress. The existing
|
|
31
|
+
// assignment fence prevents another Builder turn without reconciliation.
|
|
32
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveExecutablePath, sanitizeDiagnostics } from "../runtimeReadiness.
|
|
|
11
11
|
import { ObservabilityReader, ObservabilityStore } from "../observability.js";
|
|
12
12
|
import { WorkflowReader } from "../workflowReader.js";
|
|
13
13
|
import { WorkflowDb } from "../workflowDb.js";
|
|
14
|
+
import { error, success, warning } from "../terminalStyle.js";
|
|
14
15
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))?.version;
|
|
15
16
|
function commandVersion(command) {
|
|
16
17
|
const result = spawnSync(command, ["--version"], {
|
|
@@ -41,12 +42,12 @@ export function buildDoctorCommand() {
|
|
|
41
42
|
}
|
|
42
43
|
let errors = 0;
|
|
43
44
|
const report = (ok, label, detail) => {
|
|
44
|
-
console.log(`${ok ? "ok" : "!!"} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
45
|
+
console.log(`${ok ? success("ok") : error("!!")} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
45
46
|
if (!ok)
|
|
46
47
|
errors++;
|
|
47
48
|
};
|
|
48
49
|
const warn = (label, detail) => {
|
|
49
|
-
console.log(
|
|
50
|
+
console.log(`${warning("--")} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
50
51
|
};
|
|
51
52
|
report(Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20, "node >=20", process.version);
|
|
52
53
|
report(Boolean(PACKAGE_VERSION), "foreman package version", PACKAGE_VERSION);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { pruneGraph } from "../graph/lifecycle.js";
|
|
2
|
+
import { fileSemanticHost } from "../graph/semantic.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { assertGraphConfig, assertGraphReadRequest, DEFAULT_GRAPH_CONFIG } from "rafi-spec";
|
|
8
|
+
import { loadGraphConfig } from "../graph/config.js";
|
|
9
|
+
import { adoptGraph, disableGraph, refreshGraph, ensureGraphInstallation } from "../graph/maintenance.js";
|
|
10
|
+
import { acquireGraphView, graphStatus, readGraph } from "../graph/read.js";
|
|
11
|
+
import { captureGraphCorpus } from "../graph/corpus.js";
|
|
12
|
+
export function buildGraphCommand() {
|
|
13
|
+
const command = new Command("graph").description("Adopt and inspect scoped local Graphify evidence. Reads never install or refresh.");
|
|
14
|
+
command.command("status").argument("[project]", "project configuration root", ".").option("--json", "emit structured status").action((project) => {
|
|
15
|
+
const result = graphStatus(resolve(project));
|
|
16
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17
|
+
});
|
|
18
|
+
command.command("adopt").argument("[project]", "project configuration root", ".").option("--yes", "accept the displayed scope and selective maintenance policy").option("--config <file>", "complete graph policy JSON").option("--code-only", "explicitly omit semantic extraction").action(async (project, opts) => {
|
|
19
|
+
const root = resolve(project);
|
|
20
|
+
const config = opts.config ? JSON.parse(readFileSync(resolve(opts.config), "utf8")) : structuredClone(DEFAULT_GRAPH_CONFIG);
|
|
21
|
+
if (opts.codeOnly)
|
|
22
|
+
config.mode = "code-only";
|
|
23
|
+
assertGraphConfig(config);
|
|
24
|
+
console.log(`Graphify adoption policy:\n${JSON.stringify(config, null, 2)}\nReuses a compatible installation or installs pinned Graphify in a Rafi-owned isolated environment. Initial extraction and selective maintenance are authorized by acceptance.`);
|
|
25
|
+
if (!opts.yes) {
|
|
26
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
27
|
+
throw new Error("Adoption requires --yes and a complete policy in noninteractive mode");
|
|
28
|
+
const { confirm, isCancel } = await import("@clack/prompts");
|
|
29
|
+
const accepted = await confirm({ message: "Accept this Graphify scope and policy?", initialValue: true });
|
|
30
|
+
if (isCancel(accepted) || !accepted)
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const adoption = adoptGraph(root, { config, authorization: "explicit" });
|
|
34
|
+
const python = await ensureGraphInstallation(root);
|
|
35
|
+
const result = await refreshGraph(root, root, adoption.initialOperationId, { python });
|
|
36
|
+
console.log(JSON.stringify(result, null, 2));
|
|
37
|
+
if (result.state !== "published")
|
|
38
|
+
throw new Error(result.reason ?? "Initial graph unavailable");
|
|
39
|
+
});
|
|
40
|
+
command.command("refresh").argument("[project]", "project configuration root", ".").option("--workspace <path>", "authorized source workspace").option("--task <id>", "stable task id for idempotent retry").option("--semantic-request <file>", "write captured host extraction packet (requires --task)").option("--semantic-result <file>", "consume exact host result (requires --task)").action(async (project, opts) => {
|
|
41
|
+
if ((opts.semanticRequest || opts.semanticResult) && !opts.task)
|
|
42
|
+
throw new Error("Semantic exchange requires a stable --task ID");
|
|
43
|
+
if (opts.semanticRequest && opts.semanticResult)
|
|
44
|
+
throw new Error("Choose request or result exchange, not both");
|
|
45
|
+
const root = resolve(project), workspace = opts.workspace ? resolve(opts.workspace) : root;
|
|
46
|
+
const result = await refreshGraph(root, workspace, opts.task ?? randomUUID(), { semanticHost: fileSemanticHost({ requestPath: opts.semanticRequest ? resolve(opts.semanticRequest) : undefined, resultPath: opts.semanticResult ? resolve(opts.semanticResult) : undefined }) });
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
if (result.state !== "published")
|
|
49
|
+
throw new Error(result.reason ?? "Graph refresh unavailable");
|
|
50
|
+
});
|
|
51
|
+
command.command("prune").argument("[project]", "project configuration root", ".").action((project) => console.log(JSON.stringify(pruneGraph(resolve(project)), null, 2)));
|
|
52
|
+
command.command("disable").argument("[project]", "project configuration root", ".").action((project) => { disableGraph(resolve(project)); console.log("Graph integration disabled; retained evidence preserved."); });
|
|
53
|
+
command.command("enable").argument("[project]", "project configuration root", ".").action((project) => {
|
|
54
|
+
const root = resolve(project), prior = loadGraphConfig(root);
|
|
55
|
+
if (!prior.adoption)
|
|
56
|
+
throw new Error("No prior adoption; use graph adopt");
|
|
57
|
+
adoptGraph(root, { config: { ...prior.adoption.config, enabled: true }, authorization: "explicit" });
|
|
58
|
+
console.log("Graph integration enabled under its accepted policy.");
|
|
59
|
+
});
|
|
60
|
+
for (const operation of ["query", "node", "neighbors", "path", "impact"]) {
|
|
61
|
+
command.command(operation).argument("<query>", "query text or node id").argument("[target]", "target node for path").option("--project <path>", "configuration/source project root", ".").option("--json", "structured evidence").option("--direction <direction>", "incoming | outgoing | both", "both").action(async (query, target, opts) => {
|
|
62
|
+
const root = resolve(opts.project);
|
|
63
|
+
const op = { operation, ...(operation === "query" ? { query } : { seeds: [query] }), ...(target ? { target } : {}), direction: opts.direction };
|
|
64
|
+
assertGraphReadRequest({ kind: "rafi_graph_request", version: 1, requestId: "cli", operations: [op] });
|
|
65
|
+
const config = loadGraphConfig(root);
|
|
66
|
+
const corpus = config.enabled ? captureGraphCorpus(root, root, config) : undefined;
|
|
67
|
+
const view = acquireGraphView(root, root, corpus);
|
|
68
|
+
const result = "graph" in view ? await readGraph(view, op) : view;
|
|
69
|
+
console.log(JSON.stringify(result, null, 2));
|
|
70
|
+
if (result.status === "unavailable" || result.status === "invalid-request")
|
|
71
|
+
throw new Error("Graph evidence unavailable; see the structured limitation above");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return command;
|
|
75
|
+
}
|
package/dist/cli/manager.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ManagerEvidenceService } from "../managerEvidence.js";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import type { EffortLevel } from "../adapters/types.js";
|
|
4
|
+
import type { ManagerEvidencePageV2 } from "rafi-spec";
|
|
3
5
|
import type { ExternalDiagnosticMode } from "../diagnostics.js";
|
|
4
6
|
export interface ManagerCommandOptions {
|
|
5
7
|
resolveProject?: (project: string | undefined) => string;
|
|
@@ -15,3 +17,9 @@ export declare function runManager(projectDir: string, options: {
|
|
|
15
17
|
fast?: boolean;
|
|
16
18
|
external?: ExternalDiagnosticMode;
|
|
17
19
|
}): Promise<void>;
|
|
20
|
+
/** Only original user commands enter here. Model output and retained reports cannot export raw bytes. */
|
|
21
|
+
export declare function executeManagerHostCommand(service: ManagerEvidenceService, command: string, write: (text: string) => void, complete?: boolean): boolean;
|
|
22
|
+
/** Drain one pinned snapshot and expand every referenced artifact before host close.
|
|
23
|
+
* Errors are returned as incomplete output; an expired snapshot is never refreshed.
|
|
24
|
+
* This host output is deliberately independent of bounded model packets. */
|
|
25
|
+
export declare function completeManagerHostEvidence(service: ManagerEvidenceService, first: ManagerEvidencePageV2): ManagerEvidencePageV2;
|