@awak-app/simy-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const EXECUTION_IO_POLICY_VERSION = "execution_io_redaction.v1";
4
+
5
+ const DEFAULT_TEXT_LIMIT = 24_000;
6
+ const DEFAULT_LOG_LINE_LIMIT = 100;
7
+ const DEFAULT_LOG_LINE_CHAR_LIMIT = 2_000;
8
+ const DEFAULT_LOG_TOTAL_CHAR_LIMIT = 40_000;
9
+ const REDACTED = "[REDACTED]";
10
+
11
+ const PRIVATE_KEY_PATTERN =
12
+ /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/g;
13
+ const AUTHORIZATION_PATTERN = /(\bAuthorization\s*:\s*)(?:Bearer|Basic)\s+[^\s,;]+/gi;
14
+ const URL_CREDENTIAL_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
15
+ const SECRET_KEY =
16
+ "api[_-]?key|token|access[_-]?token|refresh[_-]?token|id[_-]?token|auth[_-]?token|session[_-]?(?:token|secret)|client[_-]?secret|secret(?:[_-]?(?:access[_-]?key|key))?|password|passwd|pwd|private[_-]?key|cookie|set-cookie";
17
+ const QUOTED_SECRET_PATTERN = new RegExp(
18
+ `(["']?(?:${SECRET_KEY})["']?\\s*[:=]\\s*)(["'])(?:\\\\.|(?!\\2).)*\\2`,
19
+ "gi",
20
+ );
21
+ const UNQUOTED_SECRET_PATTERN = new RegExp(
22
+ `((?:${SECRET_KEY})\\s*[:=]\\s*)(?!${escapeRegExp(REDACTED)})[^\\s,;]+`,
23
+ "gi",
24
+ );
25
+ const KNOWN_TOKEN_PATTERN =
26
+ /\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})\b/g;
27
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
28
+
29
+ export function redactExecutionText(value, { maxChars = DEFAULT_TEXT_LIMIT } = {}) {
30
+ const original = typeof value === "string" ? value : "";
31
+ let text = original
32
+ .replace(PRIVATE_KEY_PATTERN, REDACTED)
33
+ .replace(AUTHORIZATION_PATTERN, `$1${REDACTED}`)
34
+ .replace(URL_CREDENTIAL_PATTERN, `$1${REDACTED}@`)
35
+ .replace(QUOTED_SECRET_PATTERN, (_match, prefix, quote) => `${prefix}${quote}${REDACTED}${quote}`)
36
+ .replace(UNQUOTED_SECRET_PATTERN, `$1${REDACTED}`)
37
+ .replace(KNOWN_TOKEN_PATTERN, REDACTED)
38
+ .replace(JWT_PATTERN, REDACTED);
39
+ const redacted = text !== original;
40
+ const truncated = text.length > maxChars;
41
+ if (truncated) text = `${text.slice(0, Math.max(0, maxChars - 14))}\n[TRUNCATED]`;
42
+ return { text, redacted, truncated };
43
+ }
44
+
45
+ export function redactExecutionLogs(
46
+ value,
47
+ {
48
+ maxLines = DEFAULT_LOG_LINE_LIMIT,
49
+ maxLineChars = DEFAULT_LOG_LINE_CHAR_LIMIT,
50
+ maxTotalChars = DEFAULT_LOG_TOTAL_CHAR_LIMIT,
51
+ } = {},
52
+ ) {
53
+ const source = Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
54
+ const selected = source.slice(-maxLines);
55
+ let remaining = maxTotalChars;
56
+ let redacted = false;
57
+ let truncated = source.length > selected.length;
58
+ const lines = [];
59
+
60
+ for (let index = selected.length - 1; index >= 0 && remaining > 0; index -= 1) {
61
+ const result = redactExecutionText(selected[index], {
62
+ maxChars: Math.min(maxLineChars, remaining),
63
+ });
64
+ redacted ||= result.redacted;
65
+ truncated ||= result.truncated;
66
+ if (!result.text) continue;
67
+ lines.push(result.text);
68
+ remaining -= result.text.length;
69
+ }
70
+
71
+ lines.reverse();
72
+ if (lines.length < selected.length) truncated = true;
73
+ return { lines, redacted, truncated };
74
+ }
75
+
76
+ export function executionTextSha256(value) {
77
+ const text = typeof value === "string" ? value : "";
78
+ return text ? createHash("sha256").update(text).digest("hex") : null;
79
+ }
80
+
81
+ function escapeRegExp(value) {
82
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
83
+ }
@@ -0,0 +1,83 @@
1
+ import { bullets, cleanString, section } from "./shared.js";
2
+
3
+ export function buildIndependentAuditInstruction(charter, attempt) {
4
+ return [
5
+ section(
6
+ "role",
7
+ "You are an independent PR auditor. You did not implement this change and must not edit files.",
8
+ ),
9
+ section(
10
+ "audit_target",
11
+ [
12
+ `Requirement: ${charter.requirement}`,
13
+ `Acceptance criteria:\n${bullets(charter.acceptance_criteria)}`,
14
+ `Risk level: ${charter.risk.level}`,
15
+ `Risk tags: ${charter.risk.tags.join(", ") || "none"}`,
16
+ `Base branch: ${charter.base_branch}`,
17
+ `Commit SHA: ${attempt.observed_evidence?.local?.head_sha || attempt.commit_sha || "unknown"}`,
18
+ ].join("\n"),
19
+ ),
20
+ section(
21
+ "audit_scope",
22
+ [
23
+ "Inspect the final diff and related code directly from the repository.",
24
+ "Look for unmet acceptance criteria, regressions, security issues, data and concurrency risks, missing tests, and unrelated changes.",
25
+ "Treat executor claims as untrusted until supported by repository evidence.",
26
+ "Do not modify files, commit, push, or update the pull request.",
27
+ ].join("\n"),
28
+ ),
29
+ section(
30
+ "result_contract",
31
+ [
32
+ "Finish with exactly one line beginning SIMY_AUDIT_JSON: followed by one JSON object.",
33
+ "Required keys: passed, summary, findings.",
34
+ "Each finding must contain code, severity, target, explanation, and repairability.",
35
+ "Use passed=false when a blocker or major finding remains, or when evidence is unavailable.",
36
+ ].join("\n"),
37
+ ),
38
+ ].join("\n\n");
39
+ }
40
+
41
+ export function buildIndependentAudit(execution) {
42
+ const result = execution?.result && typeof execution.result === "object" ? execution.result : {};
43
+ const valid =
44
+ execution?.exitCode === 0 &&
45
+ !execution?.error &&
46
+ typeof result.passed === "boolean" &&
47
+ Array.isArray(result.findings);
48
+ if (!valid) {
49
+ return {
50
+ passed: false,
51
+ summary: cleanString(execution?.error || result.summary) || "Independent audit did not return valid evidence.",
52
+ findings: [
53
+ {
54
+ code: "INDEPENDENT_AUDIT_UNAVAILABLE",
55
+ severity: "blocker",
56
+ target: "independent_audit",
57
+ explanation: "The independent auditor failed or returned an invalid structured result.",
58
+ repairability: "manual",
59
+ },
60
+ ],
61
+ requires_human: true,
62
+ };
63
+ }
64
+
65
+ const findings = result.findings
66
+ .filter((item) => item && typeof item === "object")
67
+ .map((item, index) => ({
68
+ code: cleanString(item.code) || `INDEPENDENT_AUDIT_${index + 1}`,
69
+ severity: cleanString(item.severity).toLowerCase() || "major",
70
+ target: cleanString(item.target) || "pull_request",
71
+ explanation: cleanString(item.explanation) || "Independent audit finding.",
72
+ repairability: item.repairability === "manual" ? "manual" : "auto",
73
+ auto_fix_hint: cleanString(item.auto_fix_hint) || null,
74
+ }));
75
+ const blocking = findings.some((item) => ["blocker", "major"].includes(item.severity));
76
+ const requiresHuman = findings.some((item) => item.repairability === "manual");
77
+ return {
78
+ passed: result.passed === true && !blocking && !requiresHuman,
79
+ summary: cleanString(result.summary) || "Independent audit completed.",
80
+ findings,
81
+ requires_human: requiresHuman,
82
+ };
83
+ }
@@ -0,0 +1,5 @@
1
+ export { createCodingLoopSnapshot, newRunId } from "./contract.js";
2
+ export { collectPrEvidence } from "./evidence.js";
3
+ export { buildIndependentAuditInstruction } from "./independent-audit.js";
4
+ export { recheckPrReadiness, runCodingLoop } from "./loop.js";
5
+ export { parseStructuredMarker, parseStructuredResult } from "./result.js";
@@ -0,0 +1,123 @@
1
+ import { bullets, looksLikeUiTask, section } from "./shared.js";
2
+
3
+ export function buildCodingInstruction(
4
+ charter,
5
+ { attemptNumber, previousAttempt = null, previousFindings = [], promptInterventions = [] },
6
+ ) {
7
+ const sections = [
8
+ section("role", "You are the local coding executor for a SIMY coding loop."),
9
+ section(
10
+ "requirement_charter",
11
+ [
12
+ `Requirement: ${charter.requirement}`,
13
+ `Repository: ${charter.repository}`,
14
+ `Required PR base branch: ${charter.base_branch}`,
15
+ `Risk level: ${charter.risk.level}`,
16
+ `Design summary: ${charter.design_review.summary || "not required"}`,
17
+ `Design approved by: ${charter.design_review.approved_by || "not required"}`,
18
+ `Attempt: ${attemptNumber}/${charter.max_attempts}`,
19
+ `Local evidence root: ${charter.ui_evidence_root || "not required"}`,
20
+ ].join("\n"),
21
+ ),
22
+ section("acceptance_criteria", bullets(charter.acceptance_criteria)),
23
+ section("expected_tests", bullets(charter.expected_tests)),
24
+ section("must_not", bullets(charter.must_not)),
25
+ ];
26
+
27
+ if (previousAttempt || previousFindings.length > 0) {
28
+ sections.push(
29
+ section(
30
+ "previous_attempt_feedback",
31
+ [
32
+ previousAttempt ? `Previous attempt summary: ${previousAttempt.summary || "missing"}` : "",
33
+ previousAttempt ? `Previous PR: ${previousAttempt.pr_url || "missing"}` : "",
34
+ "Findings to repair:",
35
+ bullets(
36
+ previousFindings.map(
37
+ (finding) =>
38
+ `${finding.code}: ${finding.auto_fix_hint || finding.explanation || finding.title}`,
39
+ ),
40
+ ),
41
+ ]
42
+ .filter(Boolean)
43
+ .join("\n"),
44
+ ),
45
+ );
46
+ }
47
+
48
+ if (promptInterventions.length > 0) {
49
+ sections.push(
50
+ section(
51
+ "simy_interventions",
52
+ promptInterventions.map((item) => `${item.title}: ${item.prompt}`).join("\n\n"),
53
+ ),
54
+ );
55
+ }
56
+
57
+ sections.push(
58
+ section(
59
+ "execution_contract",
60
+ [
61
+ "Read the repository instructions and current git state before editing.",
62
+ "Preserve user changes and keep the diff scoped to the requirement.",
63
+ "Run relevant tests, inspect the final diff, commit, push, and create or update a PR.",
64
+ `The PR must target ${charter.base_branch}.`,
65
+ "Do not commit screenshots, videos, logs, secrets, or SIMY result files.",
66
+ ].join("\n"),
67
+ ),
68
+ section(
69
+ "result_contract",
70
+ [
71
+ "Finish with exactly one line beginning SIMY_RESULT_JSON: followed by one JSON object.",
72
+ "Required keys: outcome_kind, summary, branch_name, commit_sha, commit_message_headline, pr_url, pr_number, pr_title, pr_base_branch, tests_run, tests_passed, ui_evidence_path, unrelated_changes_detected, secret_scan_passed, changed_files, residual_risks.",
73
+ "Use null for unavailable scalar values and [] for unavailable arrays. Do not claim evidence that was not observed.",
74
+ ].join("\n"),
75
+ ),
76
+ );
77
+ return sections.join("\n\n");
78
+ }
79
+
80
+ export function buildPromptInterventions(charter, { attemptNumber, previousFindings }) {
81
+ const now = new Date().toISOString();
82
+ const rows = [
83
+ {
84
+ trigger: "purpose_lock",
85
+ title: "Purpose lock",
86
+ prompt: "Keep every edit tied to the original requirement and state any assumption explicitly.",
87
+ evidence_required: ["changed_files", "completion_evidence"],
88
+ },
89
+ {
90
+ trigger: "release_management",
91
+ title: "Release management",
92
+ prompt: "Record the branch, commit, tests, and pull request while preserving repository release rules.",
93
+ evidence_required: ["git_status", "pr_url", "commit_sha", "tests_run"],
94
+ },
95
+ {
96
+ trigger: "completion_self_audit",
97
+ title: "Completion self-audit",
98
+ prompt: "Inspect the final diff and report residual risks without claiming unobserved success.",
99
+ evidence_required: ["self_audit", "residual_risks"],
100
+ },
101
+ ];
102
+ if (looksLikeUiTask(charter.requirement)) {
103
+ rows.push({
104
+ trigger: "browser_verification",
105
+ title: "Browser verification",
106
+ prompt: "Open the local UI, exercise the changed flow, check console/network errors, and capture local evidence.",
107
+ evidence_required: ["opened_url", "operation_result", "screenshot"],
108
+ });
109
+ }
110
+ if (previousFindings.length > 0) {
111
+ rows.push({
112
+ trigger: "repair_focus",
113
+ title: "Repair focus",
114
+ prompt: "Fix only the reported gate failures, then rerun the affected verification.",
115
+ evidence_required: ["fixed_finding_codes", "rerun_report"],
116
+ });
117
+ }
118
+ return rows.map((row) => ({
119
+ id: `${charter.id}-${row.trigger}-${attemptNumber}`,
120
+ ...row,
121
+ created_at: now,
122
+ }));
123
+ }
@@ -0,0 +1,341 @@
1
+ import { auditAttempt, evaluatePrReadiness } from "./audit.js";
2
+ import {
3
+ buildIndependentAudit,
4
+ buildIndependentAuditInstruction,
5
+ } from "./independent-audit.js";
6
+ import { buildCodingInstruction, buildPromptInterventions } from "./instruction.js";
7
+ import { buildAttempt } from "./result.js";
8
+ import { appendEvent, publish } from "./shared.js";
9
+
10
+ export async function runCodingLoop({
11
+ snapshot,
12
+ executeAttempt,
13
+ executeIndependentAudit,
14
+ collectEvidence,
15
+ onUpdate,
16
+ }) {
17
+ appendEvent(snapshot, "risk_classifying", "PR risk classification completed.", {
18
+ risk: snapshot.charter.risk,
19
+ });
20
+ await publish(snapshot, "risk_classifying", onUpdate);
21
+
22
+ const charterFindings = validateCharter(snapshot.charter);
23
+ if (charterFindings.length > 0) {
24
+ snapshot.final_audit = humanGate(
25
+ "Requirements or design review must be completed before implementation.",
26
+ charterFindings,
27
+ );
28
+ appendEvent(snapshot, "waiting_human", "Requirements require human clarification.", {
29
+ finding_codes: charterFindings.map((finding) => finding.code),
30
+ });
31
+ await publish(snapshot, "waiting_human", onUpdate);
32
+ return snapshot;
33
+ }
34
+
35
+ appendEvent(snapshot, "chartering", "Requirement charter created locally.", {
36
+ charter_id: snapshot.charter.id,
37
+ base_branch: snapshot.charter.base_branch,
38
+ risk_level: snapshot.charter.risk.level,
39
+ });
40
+ await publish(snapshot, "chartering", onUpdate);
41
+
42
+ let previousFindings = [];
43
+ for (
44
+ let attemptNumber = 1;
45
+ attemptNumber <= snapshot.charter.max_attempts;
46
+ attemptNumber += 1
47
+ ) {
48
+ const promptInterventions = buildPromptInterventions(snapshot.charter, {
49
+ attemptNumber,
50
+ previousFindings,
51
+ });
52
+ const instruction = buildCodingInstruction(snapshot.charter, {
53
+ attemptNumber,
54
+ previousAttempt: snapshot.attempts.at(-1) ?? null,
55
+ previousFindings,
56
+ promptInterventions,
57
+ });
58
+
59
+ appendEvent(snapshot, "dispatching", `Dispatching local coding attempt ${attemptNumber}.`, {
60
+ backend: snapshot.charter.backend,
61
+ attempt_number: attemptNumber,
62
+ instruction,
63
+ });
64
+ await publish(snapshot, "dispatching", onUpdate);
65
+
66
+ appendEvent(snapshot, "coding", `${snapshot.charter.backend} is executing locally.`, {
67
+ backend: snapshot.charter.backend,
68
+ attempt_number: attemptNumber,
69
+ instruction,
70
+ });
71
+ await publish(snapshot, "coding", onUpdate);
72
+
73
+ const execution = await safeExecution(() =>
74
+ executeAttempt({
75
+ attemptNumber,
76
+ backend: snapshot.charter.backend,
77
+ instruction,
78
+ charter: snapshot.charter,
79
+ }),
80
+ );
81
+ const attempt = buildAttempt({
82
+ attemptNumber,
83
+ charter: snapshot.charter,
84
+ instruction,
85
+ promptInterventions,
86
+ execution,
87
+ });
88
+
89
+ appendEvent(snapshot, "collecting_evidence", `Collecting attempt ${attemptNumber} evidence.`, {
90
+ attempt_number: attemptNumber,
91
+ });
92
+ await publish(snapshot, "collecting_evidence", onUpdate);
93
+ attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
94
+
95
+ appendEvent(snapshot, "auditing", `Verifying local evidence for attempt ${attemptNumber}.`, {
96
+ attempt_number: attemptNumber,
97
+ outcome_kind: attempt.outcome_kind,
98
+ });
99
+ await publish(snapshot, "auditing", onUpdate);
100
+ attempt.audit = await auditAttempt(snapshot.charter, attempt);
101
+ attempt.implementation_gate = attempt.audit.implementation_gate;
102
+ snapshot.attempts.push(attempt);
103
+
104
+ if (!attempt.audit.passed) {
105
+ snapshot.final_audit = attempt.audit;
106
+ const terminal = await handleFailedAudit({
107
+ snapshot,
108
+ attempt,
109
+ attemptNumber,
110
+ findings: attempt.audit.findings,
111
+ requiresHuman: attempt.audit.requires_human,
112
+ onUpdate,
113
+ });
114
+ if (terminal) return snapshot;
115
+ previousFindings = attempt.audit.findings;
116
+ continue;
117
+ }
118
+
119
+ appendEvent(snapshot, "independent_auditing", "Starting an independent AI audit.", {
120
+ attempt_number: attemptNumber,
121
+ backend: snapshot.charter.audit_backend,
122
+ });
123
+ await publish(snapshot, "independent_auditing", onUpdate);
124
+ const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
125
+ const auditExecution = await safeExecution(() =>
126
+ executeIndependentAudit({
127
+ attemptNumber,
128
+ backend: snapshot.charter.audit_backend,
129
+ instruction: auditInstruction,
130
+ charter: snapshot.charter,
131
+ attempt,
132
+ }),
133
+ );
134
+ attempt.independent_audit = buildIndependentAudit(auditExecution);
135
+
136
+ // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
137
+ attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
138
+ attempt.audit = await auditAttempt(snapshot.charter, attempt);
139
+ attempt.implementation_gate = attempt.audit.implementation_gate;
140
+
141
+ const independentFindings = attempt.independent_audit.findings || [];
142
+ if (!attempt.audit.passed || !attempt.independent_audit.passed) {
143
+ const findings = [...attempt.audit.findings, ...independentFindings];
144
+ snapshot.final_audit = aggregateAudit(attempt, false, findings);
145
+ const terminal = await handleFailedAudit({
146
+ snapshot,
147
+ attempt,
148
+ attemptNumber,
149
+ findings,
150
+ requiresHuman:
151
+ attempt.audit.requires_human || attempt.independent_audit.requires_human,
152
+ onUpdate,
153
+ });
154
+ if (terminal) return snapshot;
155
+ previousFindings = findings;
156
+ continue;
157
+ }
158
+
159
+ snapshot.implementation_gate_passed = true;
160
+ attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
161
+ attempt.pr_gate = attempt.pr_readiness;
162
+ snapshot.final_audit = aggregateAudit(
163
+ attempt,
164
+ attempt.pr_readiness.ready,
165
+ attempt.independent_audit.findings || [],
166
+ );
167
+ snapshot.done_gate_passed = attempt.pr_readiness.ready;
168
+ appendEvent(snapshot, attempt.pr_readiness.state, attempt.pr_readiness.summary, {
169
+ attempt_number: attemptNumber,
170
+ pr_url: attempt.pr_url,
171
+ pending_reasons: attempt.pr_readiness.pending_reasons,
172
+ });
173
+ await publish(snapshot, attempt.pr_readiness.state, onUpdate);
174
+ return snapshot;
175
+ }
176
+
177
+ return snapshot;
178
+ }
179
+
180
+ export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }) {
181
+ const attempt = snapshot.attempts.at(-1);
182
+ if (!attempt || !attempt.audit?.passed || !attempt.independent_audit?.passed) return snapshot;
183
+ appendEvent(snapshot, "checking_pr", "Refreshing GitHub review and CI evidence.", {
184
+ attempt_number: attempt.attempt_number,
185
+ });
186
+ await publish(snapshot, "checking_pr", onUpdate);
187
+ attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
188
+ attempt.audit = await auditAttempt(snapshot.charter, attempt);
189
+ snapshot.implementation_gate_passed = attempt.audit.passed;
190
+ attempt.pr_readiness = evaluatePrReadiness(snapshot.charter, attempt);
191
+ attempt.pr_gate = attempt.pr_readiness;
192
+ snapshot.done_gate_passed = attempt.pr_readiness.ready;
193
+ snapshot.final_audit = aggregateAudit(
194
+ attempt,
195
+ attempt.pr_readiness.ready,
196
+ attempt.independent_audit.findings || [],
197
+ );
198
+ appendEvent(snapshot, attempt.pr_readiness.state, attempt.pr_readiness.summary, {
199
+ pr_url: attempt.pr_url,
200
+ pending_reasons: attempt.pr_readiness.pending_reasons,
201
+ });
202
+ await publish(snapshot, attempt.pr_readiness.state, onUpdate);
203
+ return snapshot;
204
+ }
205
+
206
+ function validateCharter(charter) {
207
+ const findings = [];
208
+ if (charter.risk.requires_design_review && charter.acceptance_criteria_source !== "explicit") {
209
+ findings.push({
210
+ code: "EXPLICIT_ACCEPTANCE_CRITERIA_REQUIRED",
211
+ severity: "blocker",
212
+ target: "acceptance_criteria",
213
+ explanation: "High-risk work requires explicit acceptance criteria.",
214
+ repairability: "manual",
215
+ });
216
+ }
217
+ if (charter.design_review.required && charter.design_review.status !== "approved") {
218
+ findings.push({
219
+ code: "DESIGN_REVIEW_REQUIRED",
220
+ severity: "blocker",
221
+ target: "design_review",
222
+ explanation:
223
+ "High-risk work requires a design summary, approval owner, and review evidence before implementation.",
224
+ repairability: "manual",
225
+ });
226
+ }
227
+ if (charter.risk.requires_design_review && charter.required_checks.length === 0) {
228
+ findings.push({
229
+ code: "REQUIRED_CI_CHECKS_MISSING",
230
+ severity: "blocker",
231
+ target: "required_checks",
232
+ explanation: "High-risk work requires explicit CI and security check names.",
233
+ repairability: "manual",
234
+ });
235
+ }
236
+ return findings;
237
+ }
238
+
239
+ async function safeExecution(callback) {
240
+ try {
241
+ return await callback();
242
+ } catch (error) {
243
+ return {
244
+ exitCode: null,
245
+ logs: [],
246
+ result: {},
247
+ error: error instanceof Error ? error.message : "Local executor failed.",
248
+ };
249
+ }
250
+ }
251
+
252
+ async function safeEvidence(collector, attempt) {
253
+ if (!collector) return unavailableEvidence("Evidence collector is not configured.");
254
+ try {
255
+ return await collector(attempt);
256
+ } catch (error) {
257
+ return unavailableEvidence(
258
+ error instanceof Error ? error.message : "Evidence collection failed.",
259
+ );
260
+ }
261
+ }
262
+
263
+ function unavailableEvidence(error) {
264
+ return {
265
+ collected_at: new Date().toISOString(),
266
+ local: {
267
+ available: false,
268
+ working_tree_clean: false,
269
+ changed_files: [],
270
+ status_lines: [],
271
+ errors: [error],
272
+ },
273
+ github: {
274
+ available: false,
275
+ checks: [],
276
+ checks_present: false,
277
+ checks_passing: false,
278
+ required_checks_present: false,
279
+ error,
280
+ },
281
+ };
282
+ }
283
+
284
+ async function handleFailedAudit({
285
+ snapshot,
286
+ attempt,
287
+ attemptNumber,
288
+ findings,
289
+ requiresHuman,
290
+ onUpdate,
291
+ }) {
292
+ snapshot.charter.thread_state.failure_count += 1;
293
+ if (requiresHuman) {
294
+ appendEvent(snapshot, "waiting_human", "PR audit requires human input.", {
295
+ finding_codes: findings.map((finding) => finding.code),
296
+ });
297
+ await publish(snapshot, "waiting_human", onUpdate);
298
+ return true;
299
+ }
300
+ if (attemptNumber < snapshot.charter.max_attempts) {
301
+ appendEvent(snapshot, "re_instructing", "PR audit produced a repair instruction.", {
302
+ attempt_number: attemptNumber,
303
+ finding_codes: findings.map((finding) => finding.code),
304
+ });
305
+ await publish(snapshot, "re_instructing", onUpdate);
306
+ return false;
307
+ }
308
+ appendEvent(snapshot, "blocked", "Coding loop exhausted its attempt budget.", {
309
+ finding_codes: findings.map((finding) => finding.code),
310
+ });
311
+ await publish(snapshot, "blocked", onUpdate);
312
+ return true;
313
+ }
314
+
315
+ function aggregateAudit(attempt, passed, findings) {
316
+ return {
317
+ passed,
318
+ summary: attempt.pr_readiness?.summary || "PR lifecycle audit completed.",
319
+ findings,
320
+ auto_repairable: false,
321
+ requires_human: !passed,
322
+ implementation_gate: attempt.implementation_gate,
323
+ independent_audit: attempt.independent_audit,
324
+ pr_gate: attempt.pr_readiness || null,
325
+ release_gate: null,
326
+ };
327
+ }
328
+
329
+ function humanGate(summary, findings) {
330
+ return {
331
+ passed: false,
332
+ summary,
333
+ findings,
334
+ auto_repairable: false,
335
+ requires_human: true,
336
+ implementation_gate: null,
337
+ independent_audit: null,
338
+ pr_gate: null,
339
+ release_gate: null,
340
+ };
341
+ }