@awak-app/simy-cli 0.1.1 → 0.1.3

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.
@@ -4,7 +4,7 @@ import {
4
4
  buildIndependentAuditInstruction,
5
5
  } from "./independent-audit.js";
6
6
  import { buildCodingInstruction, buildPromptInterventions } from "./instruction.js";
7
- import { buildAttempt } from "./result.js";
7
+ import { buildAttempt, mergeTokenUsage } from "./result.js";
8
8
  import { appendEvent, publish } from "./shared.js";
9
9
 
10
10
  export async function runCodingLoop({
@@ -13,41 +13,53 @@ export async function runCodingLoop({
13
13
  executeIndependentAudit,
14
14
  collectEvidence,
15
15
  onUpdate,
16
+ humanGuidance = "",
17
+ resume = false,
18
+ shouldStop = () => false,
19
+ consumeHumanGuidance = () => "",
16
20
  }) {
17
- appendEvent(snapshot, "risk_classifying", "PR risk classification completed.", {
18
- risk: snapshot.charter.risk,
19
- });
20
- await publish(snapshot, "risk_classifying", onUpdate);
21
+ if (!resume || snapshot.attempts.length === 0) {
22
+ appendEvent(snapshot, "risk_classifying", "PR risk classification completed.", {
23
+ risk: snapshot.charter.risk,
24
+ });
25
+ await publish(snapshot, "risk_classifying", onUpdate);
21
26
 
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),
27
+ const charterFindings = validateCharter(snapshot.charter);
28
+ if (charterFindings.length > 0) {
29
+ snapshot.final_audit = humanGate(
30
+ "Requirements or design review must be completed before implementation.",
31
+ charterFindings,
32
+ );
33
+ appendEvent(snapshot, "waiting_human", "Requirements require human clarification.", {
34
+ finding_codes: charterFindings.map((finding) => finding.code),
35
+ });
36
+ await publish(snapshot, "waiting_human", onUpdate);
37
+ return snapshot;
38
+ }
39
+
40
+ appendEvent(snapshot, "chartering", "Requirement charter created locally.", {
41
+ charter_id: snapshot.charter.id,
42
+ base_branch: snapshot.charter.base_branch,
43
+ risk_level: snapshot.charter.risk.level,
30
44
  });
31
- await publish(snapshot, "waiting_human", onUpdate);
32
- return snapshot;
45
+ await publish(snapshot, "chartering", onUpdate);
33
46
  }
34
47
 
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 = [];
48
+ let previousFindings = resume ? snapshot.final_audit?.findings || [] : [];
49
+ const firstAttempt = resume ? snapshot.attempts.length + 1 : 1;
50
+ const lastAttempt = resume ? firstAttempt : snapshot.charter.max_attempts;
43
51
  for (
44
- let attemptNumber = 1;
45
- attemptNumber <= snapshot.charter.max_attempts;
52
+ let attemptNumber = firstAttempt;
53
+ attemptNumber <= lastAttempt;
46
54
  attemptNumber += 1
47
55
  ) {
56
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
57
+ const queuedGuidance = consumeHumanGuidance();
58
+ const attemptGuidance = [humanGuidance, queuedGuidance].filter(Boolean).join("\n\n");
48
59
  const promptInterventions = buildPromptInterventions(snapshot.charter, {
49
60
  attemptNumber,
50
61
  previousFindings,
62
+ humanGuidance: attemptGuidance,
51
63
  });
52
64
  const instruction = buildCodingInstruction(snapshot.charter, {
53
65
  attemptNumber,
@@ -78,6 +90,7 @@ export async function runCodingLoop({
78
90
  charter: snapshot.charter,
79
91
  }),
80
92
  );
93
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
81
94
  const attempt = buildAttempt({
82
95
  attemptNumber,
83
96
  charter: snapshot.charter,
@@ -88,16 +101,29 @@ export async function runCodingLoop({
88
101
 
89
102
  appendEvent(snapshot, "collecting_evidence", `Collecting attempt ${attemptNumber} evidence.`, {
90
103
  attempt_number: attemptNumber,
104
+ outcome_kind: attempt.outcome_kind,
105
+ branch_name: attempt.branch_name,
106
+ commit_sha: attempt.commit_sha,
107
+ pr_url: attempt.pr_url,
108
+ tests_run: attempt.tests_run,
91
109
  });
92
110
  await publish(snapshot, "collecting_evidence", onUpdate);
93
111
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
112
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
94
113
 
95
114
  appendEvent(snapshot, "auditing", `Verifying local evidence for attempt ${attemptNumber}.`, {
96
115
  attempt_number: attemptNumber,
97
116
  outcome_kind: attempt.outcome_kind,
117
+ tests_run: attempt.tests_run,
118
+ tests_passed: attempt.tests_passed,
119
+ changed_files: attempt.raw_output?.changed_files,
120
+ evidence_available:
121
+ attempt.observed_evidence?.local?.available === true ||
122
+ attempt.observed_evidence?.github?.available === true,
98
123
  });
99
124
  await publish(snapshot, "auditing", onUpdate);
100
125
  attempt.audit = await auditAttempt(snapshot.charter, attempt);
126
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
101
127
  attempt.implementation_gate = attempt.audit.implementation_gate;
102
128
  snapshot.attempts.push(attempt);
103
129
 
@@ -121,6 +147,7 @@ export async function runCodingLoop({
121
147
  backend: snapshot.charter.audit_backend,
122
148
  });
123
149
  await publish(snapshot, "independent_auditing", onUpdate);
150
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
124
151
  const auditInstruction = buildIndependentAuditInstruction(snapshot.charter, attempt);
125
152
  const auditExecution = await safeExecution(() =>
126
153
  executeIndependentAudit({
@@ -131,11 +158,15 @@ export async function runCodingLoop({
131
158
  attempt,
132
159
  }),
133
160
  );
161
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
134
162
  attempt.independent_audit = buildIndependentAudit(auditExecution);
163
+ attempt.token_usage = mergeTokenUsage(attempt.token_usage, auditExecution.tokenUsage);
135
164
 
136
165
  // Re-collect after the read-only auditor to catch any mutated HEAD or working tree.
137
166
  attempt.observed_evidence = await safeEvidence(collectEvidence, attempt);
167
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
138
168
  attempt.audit = await auditAttempt(snapshot.charter, attempt);
169
+ if (shouldStop()) return stopCodingLoop(snapshot, onUpdate);
139
170
  attempt.implementation_gate = attempt.audit.implementation_gate;
140
171
 
141
172
  const independentFindings = attempt.independent_audit.findings || [];
@@ -177,6 +208,15 @@ export async function runCodingLoop({
177
208
  return snapshot;
178
209
  }
179
210
 
211
+ async function stopCodingLoop(snapshot, onUpdate) {
212
+ if (snapshot.state === "stopped") return snapshot;
213
+ appendEvent(snapshot, "stopped", "Coding loop stopped by the local human operator.", {
214
+ source: "local_cli",
215
+ });
216
+ await publish(snapshot, "stopped", onUpdate);
217
+ return snapshot;
218
+ }
219
+
180
220
  export async function recheckPrReadiness({ snapshot, collectEvidence, onUpdate }) {
181
221
  const attempt = snapshot.attempts.at(-1);
182
222
  if (!attempt || !attempt.audit?.passed || !attempt.independent_audit?.passed) return snapshot;
@@ -0,0 +1,189 @@
1
+ import { cleanString } from "./shared.js";
2
+
3
+ export function summarizeCodingLoopEvent(snapshot, event) {
4
+ const detail = objectValue(event?.detail);
5
+ const state = cleanString(detail.pr_lifecycle_state) || cleanString(event?.state);
6
+ const explicit = {
7
+ summary: cleanString(detail.step_summary),
8
+ result: cleanString(detail.step_result),
9
+ next: cleanString(detail.next_action),
10
+ };
11
+ if (explicit.summary) return explicit;
12
+
13
+ const charter = objectValue(snapshot?.charter);
14
+ const attemptNumber = numberValue(detail.attempt_number) || latestAttemptNumber(snapshot);
15
+ const maxAttempts = numberValue(charter.max_attempts) || 1;
16
+ const executor = executorLabel(detail.backend || charter.backend);
17
+ const repository = cleanString(charter.repository) || "the selected repository";
18
+ const branch = cleanString(detail.branch_name || charter.base_branch) || "the selected branch";
19
+ const requirement = sentencePreview(charter.requirement, 120) || "the requested change";
20
+ const findings = stringArray(detail.finding_codes);
21
+ const pendingReasons = stringArray(detail.pending_reasons);
22
+
23
+ switch (state) {
24
+ case "queued":
25
+ return {
26
+ summary: `Task recorded for local execution in ${repository} on ${branch}.`,
27
+ result: `Goal: ${requirement}`,
28
+ next: "SIMY will classify risk and turn the request into an execution charter.",
29
+ };
30
+ case "risk_classifying": {
31
+ const risk = objectValue(detail.risk || charter.risk);
32
+ const level = cleanString(risk.level) || "unknown";
33
+ return {
34
+ summary: `Risk classified as ${level}; the required review gates are now known.`,
35
+ result: risk.requires_design_review
36
+ ? "A design review is required before implementation."
37
+ : "No pre-implementation design review is required.",
38
+ next: "SIMY will finalize scope, acceptance criteria, tests, and evidence requirements.",
39
+ };
40
+ }
41
+ case "chartering":
42
+ return {
43
+ summary: `Execution scope set for: ${requirement}`,
44
+ result: `${countLabel(charter.acceptance_criteria, "acceptance criterion")}, ${countLabel(charter.expected_tests, "expected test")}, and ${countLabel(charter.expected_evidence, "evidence requirement")}.`,
45
+ next: `Attempt 1 of ${maxAttempts} will be handed to ${executor}.`,
46
+ };
47
+ case "dispatching":
48
+ return {
49
+ summary: `Preparing ${executor} attempt ${attemptNumber}/${maxAttempts} for ${branch}.`,
50
+ result: "The executor instruction now includes the task, constraints, checks, and prior findings.",
51
+ next: `${executor} will start the local coding process.`,
52
+ };
53
+ case "coding":
54
+ return {
55
+ summary: `${executor} is implementing attempt ${attemptNumber}/${maxAttempts} on ${branch}.`,
56
+ result: "Live provider output is shown below as the local process works.",
57
+ next: "When execution finishes, SIMY will collect code, test, PR, and UI evidence.",
58
+ };
59
+ case "collecting_evidence":
60
+ return {
61
+ summary: `Attempt ${attemptNumber} finished; SIMY is collecting verifiable implementation evidence.`,
62
+ result: listResult(detail.tests_run, "Checks to verify", "Checks will be read from the executor result and repository."),
63
+ next: "The evidence will be checked against the original requirement and acceptance criteria.",
64
+ };
65
+ case "auditing":
66
+ return {
67
+ summary: `SIMY is verifying attempt ${attemptNumber} against the requirement and recorded evidence.`,
68
+ result: evidenceResult(detail),
69
+ next: "A passing implementation proceeds to an independent AI review; findings trigger repair or human input.",
70
+ };
71
+ case "independent_auditing":
72
+ return {
73
+ summary: `${executorLabel(detail.backend || charter.audit_backend)} is independently reviewing attempt ${attemptNumber}.`,
74
+ result: "The reviewer uses the frozen instruction, diff, checks, and evidence rather than the implementer's conclusion.",
75
+ next: "SIMY will combine both audits and decide whether the PR is ready for human review.",
76
+ };
77
+ case "checking_pr":
78
+ return {
79
+ summary: "SIMY is refreshing the latest GitHub review and CI evidence.",
80
+ result: "The existing PR, commit, approvals, mergeability, and required checks are being re-read.",
81
+ next: "The run will move to merge-ready when every required gate passes.",
82
+ };
83
+ case "re_instructing":
84
+ return {
85
+ summary: `Attempt ${attemptNumber} needs another coding pass before review.`,
86
+ result: findingResult(findings),
87
+ next: `SIMY will give the findings to ${executor} for attempt ${Math.min(attemptNumber + 1, maxAttempts)}/${maxAttempts}.`,
88
+ };
89
+ case "pr_ready_for_review":
90
+ return {
91
+ summary: "Implementation is complete, but the PR still needs human review or external evidence.",
92
+ result: pendingReasons.length ? `Pending: ${pendingReasons.join("; ")}` : cleanString(event?.message),
93
+ next: "Review or approve the PR, then refresh its evidence from Web or CLI.",
94
+ };
95
+ case "merge_ready":
96
+ return {
97
+ summary: "Implementation, checks, evidence, and required review gates have passed.",
98
+ result: cleanString(detail.pr_url) ? `Merge-ready PR: ${detail.pr_url}` : "The recorded PR is ready to merge.",
99
+ next: "A human can merge the PR when the release timing is appropriate.",
100
+ };
101
+ case "waiting_human":
102
+ return {
103
+ summary: "The automated loop is paused because a human decision or missing input is required.",
104
+ result: findings.length ? findingResult(findings) : cleanString(snapshot?.final_audit?.summary || event?.message),
105
+ next: "Provide guidance or approval in Web or CLI to continue the same run.",
106
+ };
107
+ case "blocked":
108
+ return {
109
+ summary: "The run cannot continue automatically with its current attempt budget or evidence.",
110
+ result: findings.length ? findingResult(findings) : cleanString(event?.message),
111
+ next: "Review the findings, adjust the request or evidence, and resume the run.",
112
+ };
113
+ case "failed":
114
+ return {
115
+ summary: "Local orchestration stopped before the requested outcome was verified.",
116
+ result: cleanString(detail.error || event?.message),
117
+ next: "Fix the reported problem, then retry or continue the run.",
118
+ };
119
+ case "stopped":
120
+ return {
121
+ summary: "The local human operator stopped this coding run.",
122
+ result: "No further executor handoff will occur for this run.",
123
+ next: "Start a new run or explicitly continue this one when ready.",
124
+ };
125
+ default:
126
+ return {
127
+ summary: cleanString(event?.message) || "SIMY recorded a coding lifecycle update.",
128
+ result: "Open details for the complete structured record.",
129
+ next: "SIMY will continue from the latest recorded state.",
130
+ };
131
+ }
132
+ }
133
+
134
+ function evidenceResult(detail) {
135
+ const testsPassed = detail.tests_passed;
136
+ const changedFiles = stringArray(detail.changed_files);
137
+ const parts = [];
138
+ if (testsPassed === true) parts.push("reported checks passed");
139
+ if (testsPassed === false) parts.push("one or more reported checks failed");
140
+ if (changedFiles.length) parts.push(`${changedFiles.length} changed ${changedFiles.length === 1 ? "file" : "files"} recorded`);
141
+ return parts.length ? `${capitalize(parts.join("; "))}.` : "SIMY is checking the code result, tests, PR facts, and any required UI evidence.";
142
+ }
143
+
144
+ function findingResult(findings) {
145
+ return findings.length ? `Findings: ${findings.join(", ")}.` : "The audit recorded findings that require attention.";
146
+ }
147
+
148
+ function listResult(values, label, fallback) {
149
+ const items = stringArray(values);
150
+ return items.length ? `${label}: ${items.join(", ")}.` : fallback;
151
+ }
152
+
153
+ function countLabel(values, singular) {
154
+ const count = Array.isArray(values) ? values.length : 0;
155
+ const plural = singular === "acceptance criterion" ? "acceptance criteria" : `${singular}s`;
156
+ return `${count} ${count === 1 ? singular : plural}`;
157
+ }
158
+
159
+ function latestAttemptNumber(snapshot) {
160
+ const attempts = Array.isArray(snapshot?.attempts) ? snapshot.attempts : [];
161
+ return numberValue(attempts.at(-1)?.attempt_number) || 1;
162
+ }
163
+
164
+ function executorLabel(value) {
165
+ return cleanString(value) === "claude" ? "Claude Code" : "Codex";
166
+ }
167
+
168
+ function sentencePreview(value, maxLength) {
169
+ const text = cleanString(value).replace(/\s+/g, " ");
170
+ if (text.length <= maxLength) return text;
171
+ return `${text.slice(0, maxLength - 3).trimEnd()}...`;
172
+ }
173
+
174
+ function stringArray(value) {
175
+ return Array.isArray(value) ? value.map(cleanString).filter(Boolean) : [];
176
+ }
177
+
178
+ function numberValue(value) {
179
+ const number = Number.parseInt(String(value ?? ""), 10);
180
+ return Number.isFinite(number) && number > 0 ? number : 0;
181
+ }
182
+
183
+ function objectValue(value) {
184
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
185
+ }
186
+
187
+ function capitalize(value) {
188
+ return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
189
+ }
@@ -44,6 +44,13 @@ export function buildAttempt({ attemptNumber, charter, instruction, promptInterv
44
44
  };
45
45
  }
46
46
 
47
+ export function mergeTokenUsage(...values) {
48
+ const records = values.flatMap((value) =>
49
+ Array.isArray(value?.records) ? value.records.filter(isUsageRecord) : [],
50
+ );
51
+ return records.length > 0 ? { records } : {};
52
+ }
53
+
47
54
  export function parseStructuredResult(text) {
48
55
  return parseStructuredMarker(text, "SIMY_RESULT_JSON:");
49
56
  }
@@ -89,3 +96,7 @@ function buildArtifactPreviews(result) {
89
96
  : null,
90
97
  ].filter(Boolean);
91
98
  }
99
+
100
+ function isUsageRecord(value) {
101
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
102
+ }