codeam-cli 2.75.8 → 2.75.9

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.js +656 -148
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.75.8] — 2026-09-23
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Heartbeat failures visible + retried, exit when the backend says the session is gone
12
+
7
13
  ## [2.75.7] — 2026-09-19
8
14
 
9
15
  ### Fixed
package/dist/index.js CHANGED
@@ -2865,6 +2865,11 @@ function normalizeGuardrailPolicy(raw) {
2865
2865
  return out2;
2866
2866
  }
2867
2867
 
2868
+ // ../../packages/shared/src/packs/types.ts
2869
+ var PACK_FINDINGS_FILE = "REVIEW-FINDINGS.pack.json";
2870
+ var MAX_PACK_FINDINGS = 50;
2871
+ var PACK_HANDOFF_HEADING = "## Handoff";
2872
+
2868
2873
  // ../../packages/shared/src/packs/roles.ts
2869
2874
  var SPECIFIER_PROMPT = `# Role: Specifier
2870
2875
 
@@ -2880,6 +2885,8 @@ Method:
2880
2885
  - **Verification plan** \u2014 for each criterion, the level that proves it (unit / integration / manual) and why.
2881
2886
  3. Right-size: if the task is clearly too large for one pipeline run, narrow the criteria to a coherent first slice and record the rest under "Out of scope / next".
2882
2887
 
2888
+ 4. Close with a \`## Handoff\` section: the criteria count, what you scoped out, and any assumption the Coder must know.
2889
+
2883
2890
  Handoff bar: the spec file is committed; every acceptance criterion is testable as written; a competent implementer could start without asking you anything.`;
2884
2891
  var CODER_PROMPT = `# Role: Coder
2885
2892
 
@@ -2890,6 +2897,7 @@ Method:
2890
2897
  2. Test-first where it fits: write the test that proves a criterion, watch it fail, implement until it passes. Where strict test-first doesn't fit, still land tests alongside the change.
2891
2898
  3. Match the project's existing style, structure, and conventions. Simplest design that fully solves the problem \u2014 no speculative abstractions, no "while I'm here" changes.
2892
2899
  4. Run the project's tests / linters / build and make them pass.
2900
+ 5. Close with a \`## Handoff\` section: which criteria are implemented (by number) and where their tests live, anything you consciously did NOT do, and what the Reviewer should look at first.
2893
2901
 
2894
2902
  Handoff bar: every acceptance criterion is implemented and covered by a test; the project's checks pass; the work is committed in focused commits.`;
2895
2903
  var REVIEWER_PROMPT = `# Role: Reviewer
@@ -2905,21 +2913,42 @@ Method:
2905
2913
  - **Conventions & naming** \u2014 matches the surrounding code; names say what things are.
2906
2914
  - **Safety** \u2014 no secrets, credentials, or debugging remnants in code, tests, or fixtures.
2907
2915
  3. Fix what is justified \u2014 smallest change that resolves the finding, keeping behavior. Re-run the checks after material fixes.
2908
- 4. Record your findings honestly in your closing summary: what you found, what you fixed, what you deliberately left, and what you could not verify.
2909
-
2910
- Handoff bar: checks pass on YOUR final commit; every fix is committed; your summary lists findings \u2192 resolutions (an empty findings list must say what you checked).`;
2916
+ 4. Write your findings as DATA to \`REVIEW-FINDINGS.pack.json\` at the repo root \u2014 the next stage verifies them one by one, so what is not in this file does not get verified. Exact shape:
2917
+ \`\`\`json
2918
+ {
2919
+ "findings": [
2920
+ {
2921
+ "id": "R1",
2922
+ "severity": "blocker | major | minor | nit",
2923
+ "title": "one line: what is wrong",
2924
+ "detail": "why it matters and what you did about it",
2925
+ "file": "src/path/to/file.ts",
2926
+ "line": 42,
2927
+ "resolution": "fixed | deferred | wont_fix | needs_verification",
2928
+ "commit": "abcdef1234"
2929
+ }
2930
+ ],
2931
+ "checked": ["what you audited when the list is empty: e.g. error paths of X, test coverage of Y"]
2932
+ }
2933
+ \`\`\`
2934
+ Every finding you fixed says \`fixed\` with its commit; anything you deliberately left says \`deferred\` or \`wont_fix\` with the reason in \`detail\`; anything you could not verify says \`needs_verification\`. An empty \`findings\` array is a valid, honest result \u2014 then \`checked\` must say what you looked at. Commit this file with your stage.
2935
+ 5. Close with a \`## Handoff\` section: what you found (counts by severity), what you fixed, what you deliberately left, and what the next stage must verify.
2936
+
2937
+ Handoff bar: checks pass on YOUR final commit; every fix is committed; \`REVIEW-FINDINGS.pack.json\` is committed and lists every finding with its resolution (an empty list says what you checked).`;
2911
2938
  var QA_PROMPT = `# Role: QA
2912
2939
 
2913
2940
  You are the final gate. You verify the delivered work against the acceptance criteria as a whole \u2014 end to end, the way a demanding user would \u2014 and produce the run's closing report.
2914
2941
 
2915
2942
  Method:
2916
2943
  1. Read the task and \`SPEC.pack.md\` (when present). Your contract is the acceptance criteria; without a spec, derive them from the task.
2917
- 2. For EACH criterion, verify it against the real project: run the relevant tests, execute the code paths where feasible, inspect actual behavior/output. Do not take earlier stages' word for anything.
2918
- 3. Run the project's full checks (tests, lint, types, build) one final time.
2919
- 4. Write \`QA-REPORT.pack.md\` at the repo root: per-criterion verdict (\u2705 verified / \u26A0\uFE0F partially / \u274C failed \u2014 with evidence for each), the checks' results, anything not verifiable in this environment (stated plainly), and a short "ready to ship?" conclusion.
2920
- 5. If a criterion FAILS: fix it only when the fix is small and unambiguous; otherwise mark it failed with exact evidence \u2014 the user decides. Never paper over a failure.
2944
+ 2. Read \`REVIEW-FINDINGS.pack.json\` (when present \u2014 the Reviewer's structured findings, also summarized in your handoff input). Every finding there is an item on YOUR checklist: a \`fixed\` one must be verified fixed without regression; a \`deferred\`/\`wont_fix\` one must be judged acceptable or escalated; a \`needs_verification\` one is yours to settle.
2945
+ 3. For EACH criterion, verify it against the real project: run the relevant tests, execute the code paths where feasible, inspect actual behavior/output. Do not take earlier stages' word for anything.
2946
+ 4. Run the project's full checks (tests, lint, types, build) one final time.
2947
+ 5. Write \`QA-REPORT.pack.md\` at the repo root with two sections: **Acceptance criteria** \u2014 per-criterion verdict (\u2705 verified / \u26A0\uFE0F partially / \u274C failed \u2014 with evidence for each); **Review findings** \u2014 per finding id, verdict (\u2705 verified fixed / \u26A0\uFE0F still open / \u274C regressed \u2014 with evidence). Then the checks' results, anything not verifiable in this environment (stated plainly), and a short "ready to ship?" conclusion.
2948
+ 6. If a criterion FAILS: fix it only when the fix is small and unambiguous; otherwise mark it failed with exact evidence \u2014 the user decides. Never paper over a failure.
2949
+ 7. Close with a \`## Handoff\` section: the ship/no-ship conclusion, the failed or open items by id, and what you could not verify.
2921
2950
 
2922
- Handoff bar: the report is committed; every verdict carries evidence; the conclusion is honest about anything unverified.`;
2951
+ Handoff bar: the report is committed; every verdict carries evidence; every finding id from the Reviewer appears in the report; the conclusion is honest about anything unverified.`;
2923
2952
 
2924
2953
  // ../../packages/shared/src/packs/registry.ts
2925
2954
  var PACK_REGISTRY = {
@@ -2943,7 +2972,9 @@ var PACK_REGISTRY = {
2943
2972
  skillIds: ["code-review", "code-naming"],
2944
2973
  prompt: REVIEWER_PROMPT,
2945
2974
  // Review-only: approving clean with no change is success, not a stall.
2946
- requiresCommit: false
2975
+ requiresCommit: false,
2976
+ // Leaves REVIEW-FINDINGS.pack.json — the next stage gets a list, not prose.
2977
+ producesFindings: true
2947
2978
  }
2948
2979
  ]
2949
2980
  },
@@ -2974,7 +3005,9 @@ var PACK_REGISTRY = {
2974
3005
  skillIds: ["code-review", "code-naming"],
2975
3006
  prompt: REVIEWER_PROMPT,
2976
3007
  // Review-only: approving clean with no change is success, not a stall.
2977
- requiresCommit: false
3008
+ requiresCommit: false,
3009
+ // Leaves REVIEW-FINDINGS.pack.json — the next stage gets a list, not prose.
3010
+ producesFindings: true
2978
3011
  },
2979
3012
  {
2980
3013
  role: "qa",
@@ -3004,8 +3037,140 @@ You are ONE specialist role in a multi-role pipeline running on this repository.
3004
3037
  - **Never leave the tree broken.** Run the project's checks before finishing when the project has them; your stage ends with a working tree the next role can build on.
3005
3038
  - **Do not push, force-push, or touch remotes** \u2014 the pipeline works locally; publishing is the user's call at the end.
3006
3039
  - **Never read, edit, or commit anything under \`.codeam/\`** \u2014 that is the pipeline's own ledger, not project code.
3007
- - **Finish decisively.** When your stage's job is done and committed, say so in 2-4 lines (what you did, what you verified, anything the next stage should know) and stop. Don't ask "should I continue?" \u2014 the pipeline advances automatically.
3008
- - **If you are genuinely blocked** (contradictory requirements, missing access), say exactly what is blocking you and stop \u2014 the user is supervising and will decide.`;
3040
+ - **Finish with a \`## Handoff\` section.** When your stage's job is done and committed, end your reply with a heading \`## Handoff\` followed by 2-6 lines: what you did, what you verified, anything the next stage must know. That section \u2014 not the rest of your reply \u2014 is what the next role receives. Don't ask "should I continue?" \u2014 the pipeline advances automatically.
3041
+ - **If you need the user's decision** (contradictory requirements, a real product choice), ask ONE clear question and end your reply with 2-4 numbered options on their own lines (\`1. \u2026\`), then stop. The pipeline pauses until they answer in this conversation; keep working in it once they do, and commit as usual.
3042
+ - **If you are genuinely blocked** (missing access, a broken environment you cannot fix), say exactly what is blocking you and stop \u2014 the user is supervising and will decide.`;
3043
+
3044
+ // ../../packages/shared/src/packs/findings.ts
3045
+ var SEVERITIES = /* @__PURE__ */ new Set([
3046
+ "blocker",
3047
+ "major",
3048
+ "minor",
3049
+ "nit"
3050
+ ]);
3051
+ var RESOLUTIONS = /* @__PURE__ */ new Set([
3052
+ "fixed",
3053
+ "deferred",
3054
+ "wont_fix",
3055
+ "needs_verification"
3056
+ ]);
3057
+ var TITLE_MAX = 200;
3058
+ var DETAIL_MAX = 1200;
3059
+ function parsePackFindings(raw) {
3060
+ const text = stripFence(raw).trim();
3061
+ if (text.length === 0) return { ok: false, error: "findings file is empty" };
3062
+ let data;
3063
+ try {
3064
+ data = JSON.parse(text);
3065
+ } catch {
3066
+ const salvaged = salvageJson(text);
3067
+ if (salvaged === null) return { ok: false, error: "findings file is not valid JSON" };
3068
+ data = salvaged;
3069
+ }
3070
+ const container = Array.isArray(data) ? { findings: data } : data;
3071
+ if (!isRecord(container) || !Array.isArray(container.findings)) {
3072
+ return { ok: false, error: 'findings file has no "findings" array' };
3073
+ }
3074
+ const findings = [];
3075
+ let dropped = 0;
3076
+ const seen = /* @__PURE__ */ new Set();
3077
+ for (const entry of container.findings) {
3078
+ const f = coerceFinding(entry, findings.length + 1);
3079
+ if (!f) {
3080
+ dropped += 1;
3081
+ continue;
3082
+ }
3083
+ if (seen.has(f.id)) f.id = `${f.id}-${findings.length + 1}`;
3084
+ seen.add(f.id);
3085
+ findings.push(f);
3086
+ if (findings.length >= MAX_PACK_FINDINGS) break;
3087
+ }
3088
+ const truncated = container.findings.length - dropped - findings.length;
3089
+ const checked = Array.isArray(container.checked) ? container.checked.filter((c2) => typeof c2 === "string" && c2.trim().length > 0).slice(0, 20) : void 0;
3090
+ const notes = [];
3091
+ if (dropped > 0) notes.push(`${dropped} malformed entr${dropped === 1 ? "y" : "ies"} dropped`);
3092
+ if (truncated > 0)
3093
+ notes.push(`list truncated to ${MAX_PACK_FINDINGS} (${truncated} more in the file)`);
3094
+ return {
3095
+ ok: true,
3096
+ value: {
3097
+ findings,
3098
+ ...checked && checked.length > 0 ? { checked } : {},
3099
+ ...notes.length > 0 ? { note: notes.join("; ") } : {}
3100
+ }
3101
+ };
3102
+ }
3103
+ function coerceFinding(entry, ordinal) {
3104
+ if (!isRecord(entry)) return null;
3105
+ const title = str(entry.title) ?? str(entry.summary);
3106
+ if (!title) return null;
3107
+ const id = str(entry.id) ?? `R${ordinal}`;
3108
+ const severityRaw = (str(entry.severity) ?? "").toLowerCase();
3109
+ const resolutionRaw = (str(entry.resolution) ?? str(entry.status) ?? "").toLowerCase().replace(/[\s-]+/g, "_");
3110
+ const line = typeof entry.line === "number" && Number.isFinite(entry.line) && entry.line > 0 ? Math.floor(entry.line) : void 0;
3111
+ const detail = str(entry.detail) ?? str(entry.description);
3112
+ const file = str(entry.file) ?? str(entry.path);
3113
+ const commit = str(entry.commit);
3114
+ return {
3115
+ id: id.slice(0, 32),
3116
+ severity: SEVERITIES.has(severityRaw) ? severityRaw : "major",
3117
+ title: title.slice(0, TITLE_MAX),
3118
+ ...detail ? { detail: detail.slice(0, DETAIL_MAX) } : {},
3119
+ ...file ? { file: file.slice(0, 300) } : {},
3120
+ ...line !== void 0 ? { line } : {},
3121
+ resolution: RESOLUTIONS.has(resolutionRaw) ? resolutionRaw : "needs_verification",
3122
+ ...commit && /^[0-9a-f]{7,40}$/i.test(commit) ? { commit: commit.slice(0, 10) } : {}
3123
+ };
3124
+ }
3125
+ function extractHandoffSection(reply) {
3126
+ const lines = reply.split("\n");
3127
+ const want = PACK_HANDOFF_HEADING.replace(/^#+\s*/, "").toLowerCase();
3128
+ let start2 = -1;
3129
+ let level = 0;
3130
+ for (let i = lines.length - 1; i >= 0; i--) {
3131
+ const m = lines[i].match(/^\s{0,3}(#{1,4})\s+(.+?)\s*#*\s*$/);
3132
+ if (m && m[2].trim().toLowerCase().replace(/[:.]+$/, "") === want) {
3133
+ start2 = i;
3134
+ level = m[1].length;
3135
+ break;
3136
+ }
3137
+ }
3138
+ if (start2 < 0) return null;
3139
+ const body = [];
3140
+ for (let i = start2 + 1; i < lines.length; i++) {
3141
+ const m = lines[i].match(/^\s{0,3}(#{1,6})\s+/);
3142
+ if (m && m[1].length <= level) break;
3143
+ body.push(lines[i]);
3144
+ }
3145
+ const text = body.join("\n").trim();
3146
+ return text.length > 0 ? text : null;
3147
+ }
3148
+ function isRecord(v) {
3149
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3150
+ }
3151
+ function str(v) {
3152
+ return typeof v === "string" && v.trim().length > 0 ? v.trim() : void 0;
3153
+ }
3154
+ function stripFence(raw) {
3155
+ const m = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
3156
+ return m ? m[1] : raw;
3157
+ }
3158
+ function salvageJson(text) {
3159
+ for (const [open, close] of [
3160
+ ["{", "}"],
3161
+ ["[", "]"]
3162
+ ]) {
3163
+ const a = text.indexOf(open);
3164
+ const b = text.lastIndexOf(close);
3165
+ if (a >= 0 && b > a) {
3166
+ try {
3167
+ return JSON.parse(text.slice(a, b + 1));
3168
+ } catch {
3169
+ }
3170
+ }
3171
+ }
3172
+ return null;
3173
+ }
3009
3174
 
3010
3175
  // ../../packages/shared/src/api-url.ts
3011
3176
  var DEFAULT_API_BASE_URL = "https://api.codeagent-mobile.com";
@@ -8689,7 +8854,7 @@ var TOPLEVEL_SENTINELS = [
8689
8854
  field: "window_id"
8690
8855
  }
8691
8856
  ];
8692
- function isRecord(value) {
8857
+ function isRecord2(value) {
8693
8858
  return "object" == typeof value && null !== value && !Array.isArray(value);
8694
8859
  }
8695
8860
  function toRfc3339(timestamp) {
@@ -8708,7 +8873,7 @@ function relocateInto(properties, key, value) {
8708
8873
  if (void 0 !== value && !(key in properties)) properties[key] = value;
8709
8874
  }
8710
8875
  function buildV1Event(message) {
8711
- const sourceProperties = isRecord(message.properties) ? message.properties : {};
8876
+ const sourceProperties = isRecord2(message.properties) ? message.properties : {};
8712
8877
  const properties = {
8713
8878
  ...sourceProperties
8714
8879
  };
@@ -10344,7 +10509,7 @@ function readAnonId() {
10344
10509
  }
10345
10510
  function superProperties() {
10346
10511
  return {
10347
- cliVersion: true ? "2.75.8" : "0.0.0-dev",
10512
+ cliVersion: true ? "2.75.9" : "0.0.0-dev",
10348
10513
  nodeVersion: process.version,
10349
10514
  platform: process.platform,
10350
10515
  arch: process.arch,
@@ -10586,7 +10751,7 @@ var http = __toESM(require("http"));
10586
10751
  // package.json
10587
10752
  var package_default = {
10588
10753
  name: "codeam-cli",
10589
- version: "2.75.8",
10754
+ version: "2.75.9",
10590
10755
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
10591
10756
  type: "commonjs",
10592
10757
  main: "dist/index.js",
@@ -11384,7 +11549,7 @@ var streamHost = new StreamHostSelector(
11384
11549
  );
11385
11550
 
11386
11551
  // src/services/command-relay.service.ts
11387
- function isRecord2(v) {
11552
+ function isRecord3(v) {
11388
11553
  return typeof v === "object" && v !== null;
11389
11554
  }
11390
11555
  function httpStatusOf(err) {
@@ -11916,14 +12081,14 @@ var CommandRelayService = class _CommandRelayService {
11916
12081
  // fresh + clear the "CLI update available" banner after a self-update
11917
12082
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
11918
12083
  // pair/reconnect). Older backends ignore the extra field.
11919
- ..."2.75.8" ? { ideVersion: "2.75.8" } : {}
12084
+ ..."2.75.9" ? { ideVersion: "2.75.9" } : {}
11920
12085
  };
11921
12086
  try {
11922
12087
  const res = await _postJson(`${API_BASE2}/api/plugin/heartbeat`, body);
11923
12088
  this.heartbeatFailStreak = 0;
11924
12089
  log.trace("relay", `heartbeat ok online=${online}`);
11925
12090
  const data = res?.data;
11926
- if (online && isRecord2(data) && data.paired === false) this.handleSessionGone();
12091
+ if (online && isRecord3(data) && data.paired === false) this.handleSessionGone();
11927
12092
  } catch (err) {
11928
12093
  if (!online) {
11929
12094
  log.trace("relay", "goodbye heartbeat failed", err);
@@ -31297,14 +31462,14 @@ var zGuardCreateElicitationResponseCancel = z7.object({
31297
31462
 
31298
31463
  // ../../node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js
31299
31464
  var CANCEL_REQUEST_METHOD = "$/cancel_request";
31300
- function isRecord3(value) {
31465
+ function isRecord4(value) {
31301
31466
  return typeof value === "object" && value !== null;
31302
31467
  }
31303
31468
  function isJsonRpcId(value) {
31304
31469
  return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
31305
31470
  }
31306
31471
  function cancelRequestId(params) {
31307
- if (!isRecord3(params) || !isJsonRpcId(params["requestId"])) {
31472
+ if (!isRecord4(params) || !isJsonRpcId(params["requestId"])) {
31308
31473
  return void 0;
31309
31474
  }
31310
31475
  return params["requestId"];
@@ -31736,7 +31901,7 @@ var Connection = class {
31736
31901
  if (this.abortController.signal.aborted) {
31737
31902
  return;
31738
31903
  }
31739
- if (!isRecord3(message)) {
31904
+ if (!isRecord4(message)) {
31740
31905
  console.error("Invalid message", { message });
31741
31906
  return;
31742
31907
  }
@@ -31829,7 +31994,7 @@ var Connection = class {
31829
31994
  pendingResponse.cleanup?.();
31830
31995
  if ("result" in response) {
31831
31996
  pendingResponse.resolve(response.result);
31832
- } else if ("error" in response && isRecord3(response.error)) {
31997
+ } else if ("error" in response && isRecord4(response.error)) {
31833
31998
  const { code, message, data } = response.error;
31834
31999
  pendingResponse.reject(new RequestError(code, message, data));
31835
32000
  } else {
@@ -32102,7 +32267,7 @@ function ndJsonStream(output, input) {
32102
32267
  if (trimmedLine) {
32103
32268
  try {
32104
32269
  const message = JSON.parse(trimmedLine);
32105
- if (isRecord3(message)) {
32270
+ if (isRecord4(message)) {
32106
32271
  controller.enqueue(message);
32107
32272
  } else {
32108
32273
  console.warn("Skipping JSON line that is not an object:", trimmedLine);
@@ -37356,11 +37521,16 @@ async function postCredentialSync(opts, fetchImpl = fetch) {
37356
37521
  var import_node_child_process22 = require("child_process");
37357
37522
  var fs60 = __toESM(require("fs"));
37358
37523
  var path70 = __toESM(require("path"));
37359
- var defaultCommandRunner = (file, args2, cwd, timeoutMs) => new Promise((resolve11) => {
37524
+ var defaultCommandRunner = (file, args2, cwd, timeoutMs, env) => new Promise((resolve11) => {
37360
37525
  (0, import_node_child_process22.execFile)(
37361
37526
  file,
37362
37527
  args2,
37363
- { cwd, timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
37528
+ {
37529
+ cwd,
37530
+ timeout: timeoutMs,
37531
+ maxBuffer: 4 * 1024 * 1024,
37532
+ env: env ? { ...process.env, ...env } : process.env
37533
+ },
37364
37534
  (err, stdout, stderr) => {
37365
37535
  let code = 0;
37366
37536
  if (err) {
@@ -37377,7 +37547,12 @@ async function gitHead(run, cwd) {
37377
37547
  return res.code === 0 ? res.stdout.trim() : null;
37378
37548
  }
37379
37549
  async function canonicalCommit(run, cwd, sha) {
37380
- const verify = await run("git", ["rev-parse", "--verify", `${sha}^{commit}`], cwd, GIT_TIMEOUT_MS);
37550
+ const verify = await run(
37551
+ "git",
37552
+ ["rev-parse", "--verify", `${sha}^{commit}`],
37553
+ cwd,
37554
+ GIT_TIMEOUT_MS
37555
+ );
37381
37556
  if (verify.code !== 0) return null;
37382
37557
  const short = await run("git", ["rev-parse", "--short=10", sha], cwd, GIT_TIMEOUT_MS);
37383
37558
  return short.code === 0 ? short.stdout.trim() : null;
@@ -37388,6 +37563,18 @@ async function diffStat(run, cwd, from, to) {
37388
37563
  const lines = res.stdout.trim().split("\n").filter(Boolean);
37389
37564
  return lines.length > 0 ? lines[lines.length - 1].trim() : "";
37390
37565
  }
37566
+ async function changedFiles(run, cwd, from, to) {
37567
+ const res = await run("git", ["diff", "--name-only", `${from}..${to}`], cwd, GIT_TIMEOUT_MS);
37568
+ if (res.code !== 0) return [];
37569
+ return res.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
37570
+ }
37571
+ function readWorkspaceFile(cwd, relPath) {
37572
+ try {
37573
+ return fs60.readFileSync(path70.join(cwd, relPath), "utf8");
37574
+ } catch {
37575
+ return null;
37576
+ }
37577
+ }
37391
37578
  var NO_TEST_PLACEHOLDER = 'echo "Error: no test specified"';
37392
37579
  var CHECKS_TIMEOUT_MS = 5 * 6e4;
37393
37580
  function detectChecksCommand(cwd) {
@@ -37409,7 +37596,7 @@ function detectChecksCommand(cwd) {
37409
37596
  return null;
37410
37597
  }
37411
37598
  async function runChecks(run, cwd, command2) {
37412
- const res = await run("sh", ["-c", command2], cwd, CHECKS_TIMEOUT_MS);
37599
+ const res = await run("sh", ["-c", command2], cwd, CHECKS_TIMEOUT_MS, { CI: "1" });
37413
37600
  const combined = `${res.stdout}
37414
37601
  ${res.stderr}`.trim();
37415
37602
  const tail = combined.split("\n").slice(-12).join("\n").slice(-1500);
@@ -37506,33 +37693,109 @@ async function postPackState(opts, state, fetchImpl = fetch) {
37506
37693
  }
37507
37694
 
37508
37695
  // src/packs/runner.ts
37509
- var NUDGE_PROMPT = "Your stage is not committed yet. Commit your completed work now (focused commits, ending with your role byline `By <role>.` on its own line), then summarize in 2-4 lines and stop. If you are blocked, say exactly what is blocking you instead.";
37510
- var SUMMARY_MAX_CHARS = 600;
37511
- function composeStagePrompt(pack, stageIndex, task, previous) {
37696
+ var TERMINAL_PACK_STATUSES = /* @__PURE__ */ new Set([
37697
+ "completed",
37698
+ "aborted",
37699
+ "failed"
37700
+ ]);
37701
+ var NUDGE_PROMPT = "Your stage is not committed yet. Commit your completed work now (focused commits, ending with your role byline `By <role>.` on its own line), then close with your `## Handoff` section and stop. If you are blocked, say exactly what is blocking you instead.";
37702
+ var SUMMARY_TAIL_MAX_CHARS = 600;
37703
+ var SUMMARY_SECTION_MAX_CHARS = 1500;
37704
+ var EARLIER_HANDOFF_MAX_CHARS = 400;
37705
+ function composeStagePrompt(pack, stageIndex, task, ctx) {
37512
37706
  const stage = pack.stages[stageIndex];
37513
37707
  const pipeline2 = pack.stages.map((s, i) => i === stageIndex ? `[${s.name}]` : s.name).join(" \u2192 ");
37708
+ const position = [
37709
+ `${pack.name}: ${pipeline2} \u2014 you are stage ${stageIndex + 1} of ${pack.stages.length}. Your commit byline: \`By ${stage.role}.\``
37710
+ ];
37711
+ if (ctx.baseCommit) {
37712
+ position.push(
37713
+ `Pipeline base commit: \`${ctx.baseCommit}\` \u2014 everything the pipeline has done so far is \`git log ${ctx.baseCommit}..HEAD\` / \`git diff ${ctx.baseCommit}..HEAD\`. Use that range; do not guess it.`
37714
+ );
37715
+ }
37514
37716
  const parts = [
37515
37717
  stage.prompt,
37516
37718
  PACK_WORKFLOW_ARTICLE,
37517
37719
  `## Your pipeline position
37518
- ${pack.name}: ${pipeline2} \u2014 you are stage ${stageIndex + 1} of ${pack.stages.length}. Your commit byline: \`By ${stage.role}.\``,
37720
+ ${position.join("\n")}`,
37519
37721
  `## Task
37520
37722
  ${task}`
37521
37723
  ];
37522
- if (previous) {
37724
+ const done = ctx.prior.filter((p2) => p2.status === "done" && p2.handoff);
37725
+ const skipped = ctx.prior.filter((p2) => p2.status === "skipped");
37726
+ if (done.length > 0) {
37727
+ const last = done[done.length - 1];
37728
+ const earlier = done.slice(0, -1);
37729
+ if (earlier.length > 0) {
37730
+ parts.push(
37731
+ `## Earlier handoffs
37732
+ ${earlier.map((p2) => renderHandoff(p2, EARLIER_HANDOFF_MAX_CHARS)).join("\n\n")}`
37733
+ );
37734
+ }
37523
37735
  parts.push(
37524
- `## Previous stage handoff (${previous.role})
37525
- commit: ${previous.handoff.commit}
37526
- ${previous.handoff.diffStat}
37527
-
37528
- ${previous.handoff.summary}`
37736
+ `## Previous stage handoff (${last.role})
37737
+ ${renderHandoff(last, SUMMARY_SECTION_MAX_CHARS)}`
37738
+ );
37739
+ }
37740
+ if (skipped.length > 0) {
37741
+ parts.push(
37742
+ `## Skipped stages
37743
+ ${skipped.map((p2) => `- ${p2.name} (${p2.role}) was skipped by the user \u2014 its artifacts do not exist; do not assume them.`).join("\n")}`
37744
+ );
37745
+ }
37746
+ if (ctx.attempt > 1) {
37747
+ parts.push(
37748
+ `## Previous attempt of this stage
37749
+ Attempt ${ctx.attempt - 1} did not hand off${ctx.previousAttemptError ? `: ${ctx.previousAttemptError}` : "."}
37750
+ The working tree may contain that attempt's partial work. Inspect it first, keep what is right, and finish the stage properly.`
37529
37751
  );
37530
37752
  }
37531
37753
  return parts.join("\n\n");
37532
37754
  }
37755
+ function renderHandoff(p2, summaryMax) {
37756
+ const h = p2.handoff;
37757
+ const head = [`### ${p2.name} (${p2.role})`, `commit: ${h.commit}`];
37758
+ if (h.diffStat) head.push(h.diffStat);
37759
+ if (h.checks)
37760
+ head.push(`checks (\`${h.checks.command}\`): ${h.checks.passed ? "passed" : "FAILED"}`);
37761
+ const lines = [head.join("\n")];
37762
+ const summary = h.summary.trim();
37763
+ if (summary) lines.push(summary.length > summaryMax ? `\u2026${summary.slice(-summaryMax)}` : summary);
37764
+ if (h.checks && !h.checks.passed && h.checks.tail) {
37765
+ lines.push(`Checks output (tail):
37766
+ ${h.checks.tail}`);
37767
+ }
37768
+ if (h.findings) {
37769
+ lines.push(renderFindings(h.findings, h.findingsNote));
37770
+ } else if (h.findingsNote) {
37771
+ lines.push(`Structured findings: ${h.findingsNote}`);
37772
+ }
37773
+ return lines.join("\n\n");
37774
+ }
37775
+ function renderFindings(findings, note) {
37776
+ const list = findings ?? [];
37777
+ const header = `Structured findings (${PACK_FINDINGS_FILE}): ${list.length}${note ? ` \u2014 ${note}` : ""}`;
37778
+ if (list.length === 0) return header;
37779
+ const rows = list.map((f) => {
37780
+ const where = f.file ? ` @ ${f.file}${f.line ? `:${f.line}` : ""}` : "";
37781
+ return `- [${f.id}] ${f.severity.toUpperCase()} \u2014 ${f.title}${where} \u2192 ${f.resolution}${f.commit ? ` (${f.commit})` : ""}`;
37782
+ });
37783
+ return `${header}
37784
+ ${rows.join("\n")}`;
37785
+ }
37786
+ function summarizeReply(text) {
37787
+ const section = extractHandoffSection(text);
37788
+ if (section)
37789
+ return section.length > SUMMARY_SECTION_MAX_CHARS ? section.slice(0, SUMMARY_SECTION_MAX_CHARS) : section;
37790
+ return text.trim().slice(-SUMMARY_TAIL_MAX_CHARS);
37791
+ }
37533
37792
  function nowIso() {
37534
37793
  return (/* @__PURE__ */ new Date()).toISOString();
37535
37794
  }
37795
+ function sameCommit(a, b) {
37796
+ if (!a || !b) return a === b;
37797
+ return a.startsWith(b) || b.startsWith(a);
37798
+ }
37536
37799
  var PackRunner = class _PackRunner {
37537
37800
  constructor(deps, pack, initial) {
37538
37801
  this.deps = deps;
@@ -37544,6 +37807,10 @@ var PackRunner = class _PackRunner {
37544
37807
  state;
37545
37808
  control = "run";
37546
37809
  looping = false;
37810
+ inTurn = false;
37811
+ /** Next `runStage` continues the current stage in place (no new prompt). */
37812
+ resumeInPlace = false;
37813
+ mountedSkills = [];
37547
37814
  static create(deps, packId, task, runId) {
37548
37815
  const pack = getPackDefinition(packId);
37549
37816
  if (!pack) throw new Error(`unknown pack: ${packId}`);
@@ -37564,9 +37831,51 @@ var PackRunner = class _PackRunner {
37564
37831
  };
37565
37832
  return new _PackRunner(deps, pack, state);
37566
37833
  }
37834
+ /**
37835
+ * Rebuild a runner from the workspace ledger after a CLI restart. The
37836
+ * in-memory run died with the process; its conversation is gone. A stage
37837
+ * that was mid-turn is marked failed with the real reason (never left
37838
+ * "active" forever), and the run lands paused/stalled so Resume / Retry /
37839
+ * Skip / Abort all work again. Null when the pack id is unknown.
37840
+ */
37841
+ static rehydrate(deps, stored) {
37842
+ const pack = getPackDefinition(stored.packId);
37843
+ if (!pack) return null;
37844
+ const interrupted = stored.stages.some((s) => s.status === "active");
37845
+ const stages = stored.stages.map(
37846
+ (s) => s.status === "active" ? {
37847
+ ...s,
37848
+ status: "failed",
37849
+ error: "the CLI restarted while this stage was running",
37850
+ conversationId: void 0,
37851
+ awaitingUser: void 0
37852
+ } : s
37853
+ );
37854
+ let status2 = stored.status;
37855
+ let stalledReason = stored.stalledReason;
37856
+ if (interrupted) {
37857
+ status2 = "stalled";
37858
+ stalledReason = "The CLI restarted mid-stage. Resume re-checks whether the stage committed its work; Retry stage runs it again in a fresh conversation.";
37859
+ } else if (status2 === "running") {
37860
+ status2 = "paused";
37861
+ stalledReason = "The CLI restarted between stages \u2014 tap Resume to continue.";
37862
+ }
37863
+ const state = {
37864
+ ...stored,
37865
+ stages,
37866
+ status: status2,
37867
+ stalledReason,
37868
+ pendingControl: void 0
37869
+ };
37870
+ return new _PackRunner(deps, pack, state);
37871
+ }
37567
37872
  getState() {
37568
37873
  return this.state;
37569
37874
  }
37875
+ /** Persist + post the current state without changing it (rehydration announce). */
37876
+ async announce() {
37877
+ await this.publish();
37878
+ }
37570
37879
  /** Persist + post the current state (ledger first — it's the truth). */
37571
37880
  async publish() {
37572
37881
  this.state = { ...this.state, updatedAt: nowIso() };
@@ -37578,64 +37887,101 @@ var PackRunner = class _PackRunner {
37578
37887
  await this.deps.postState(this.state);
37579
37888
  }
37580
37889
  async settle(status2, stalledReason) {
37581
- this.state = { ...this.state, status: status2, stalledReason };
37890
+ this.state = { ...this.state, status: status2, stalledReason, pendingControl: void 0 };
37582
37891
  await this.publish();
37583
37892
  }
37893
+ patchStage(index, patch) {
37894
+ const stages = this.state.stages.slice();
37895
+ stages[index] = { ...stages[index], ...patch };
37896
+ this.state = { ...this.state, stages };
37897
+ }
37584
37898
  // ── Control surface (relay `pack_action`) ────────────────────────────────
37585
37899
  async applyAction(action) {
37900
+ const status2 = this.state.status;
37901
+ const idx = this.state.currentStage;
37902
+ const reject = (why) => ({ state: this.state, rejected: why });
37903
+ if (TERMINAL_PACK_STATUSES.has(status2)) return reject(`the run already finished (${status2})`);
37586
37904
  switch (action) {
37587
- case "pause":
37905
+ case "pause": {
37906
+ if (status2 === "paused" || status2 === "stalled") return { state: this.state };
37588
37907
  this.control = "pause";
37589
- if (this.state.status === "running") {
37590
- this.state = { ...this.state, status: "paused" };
37591
- await this.publish();
37592
- }
37908
+ this.state = { ...this.state, pendingControl: "pause" };
37909
+ await this.publish();
37593
37910
  break;
37594
- case "resume":
37911
+ }
37912
+ case "resume": {
37913
+ if (status2 !== "paused" && status2 !== "stalled")
37914
+ return reject(`nothing to resume \u2014 the run is ${status2}`);
37595
37915
  this.control = "run";
37596
- if (this.state.status === "paused" || this.state.status === "stalled") {
37597
- this.state = { ...this.state, status: "running", stalledReason: void 0 };
37598
- await this.publish();
37599
- void this.run();
37600
- }
37916
+ const stage = this.state.stages[idx];
37917
+ this.resumeInPlace = !!stage && (stage.awaitingUser === true || stage.status === "failed");
37918
+ this.state = {
37919
+ ...this.state,
37920
+ status: "running",
37921
+ stalledReason: void 0,
37922
+ pendingControl: void 0
37923
+ };
37924
+ await this.publish();
37925
+ void this.run();
37601
37926
  break;
37927
+ }
37602
37928
  case "retry_stage": {
37603
- const idx = this.state.currentStage;
37604
- if (idx < this.state.stages.length) {
37605
- const stages = this.state.stages.slice();
37606
- stages[idx] = { role: stages[idx].role, name: stages[idx].name, status: "pending" };
37607
- this.control = "run";
37608
- this.state = { ...this.state, stages, status: "running", stalledReason: void 0 };
37609
- await this.publish();
37610
- void this.run();
37611
- }
37929
+ if (status2 === "running" || this.inTurn)
37930
+ return reject("the stage is still running \u2014 pause or abort first");
37931
+ if (idx >= this.state.stages.length) return reject("no stage left to retry");
37932
+ const prev = this.state.stages[idx];
37933
+ this.patchStage(idx, {
37934
+ status: "pending",
37935
+ conversationId: void 0,
37936
+ handoff: void 0,
37937
+ awaitingUser: void 0,
37938
+ startCommit: void 0,
37939
+ attempts: prev.attempts,
37940
+ error: prev.error
37941
+ });
37942
+ this.resumeInPlace = false;
37943
+ this.control = "run";
37944
+ this.state = {
37945
+ ...this.state,
37946
+ status: "running",
37947
+ stalledReason: void 0,
37948
+ pendingControl: void 0
37949
+ };
37950
+ await this.publish();
37951
+ void this.run();
37612
37952
  break;
37613
37953
  }
37614
37954
  case "skip_stage": {
37615
- const idx = this.state.currentStage;
37616
- if (idx < this.state.stages.length) {
37617
- const stages = this.state.stages.slice();
37618
- stages[idx] = { ...stages[idx], status: "skipped" };
37619
- this.control = "run";
37620
- this.state = {
37621
- ...this.state,
37622
- stages,
37623
- currentStage: idx + 1,
37624
- status: "running",
37625
- stalledReason: void 0
37626
- };
37627
- await this.publish();
37628
- void this.run();
37629
- }
37955
+ if (status2 === "running" || this.inTurn)
37956
+ return reject("the stage is still running \u2014 pause or abort first");
37957
+ if (idx >= this.state.stages.length) return reject("no stage left to skip");
37958
+ this.patchStage(idx, { status: "skipped", awaitingUser: void 0, error: void 0 });
37959
+ this.resumeInPlace = false;
37960
+ this.control = "run";
37961
+ this.state = {
37962
+ ...this.state,
37963
+ currentStage: idx + 1,
37964
+ status: "running",
37965
+ stalledReason: void 0,
37966
+ pendingControl: void 0
37967
+ };
37968
+ await this.publish();
37969
+ void this.run();
37630
37970
  break;
37631
37971
  }
37632
- case "abort":
37972
+ case "abort": {
37633
37973
  this.control = "abort";
37634
- await this.deps.driver.cancel().catch(() => void 0);
37635
- if (!this.looping) await this.settle("aborted");
37974
+ if (this.looping) {
37975
+ this.state = { ...this.state, pendingControl: "abort" };
37976
+ await this.publish();
37977
+ await this.deps.driver.cancel().catch(() => void 0);
37978
+ } else {
37979
+ await this.settle("aborted");
37980
+ }
37636
37981
  break;
37982
+ }
37637
37983
  }
37638
- return this.state;
37984
+ return { state: this.state };
37639
37985
  }
37640
37986
  // ── The loop ──────────────────────────────────────────────────────────────
37641
37987
  async run() {
@@ -37660,104 +38006,225 @@ var PackRunner = class _PackRunner {
37660
38006
  await this.settle("failed", err.message).catch(() => void 0);
37661
38007
  } finally {
37662
38008
  this.looping = false;
38009
+ this.inTurn = false;
37663
38010
  }
37664
38011
  }
37665
38012
  /** Run one stage to its handoff. True = advanced; false = run settled. */
37666
38013
  async runStage(index) {
37667
38014
  const stageDef = this.pack.stages[index];
38015
+ const inPlace = this.resumeInPlace;
38016
+ this.resumeInPlace = false;
37668
38017
  const startedMs = Date.now();
37669
- const startSha = await this.deps.gates.head();
37670
- let stages = this.state.stages.slice();
37671
- stages[index] = { ...stages[index], status: "active" };
37672
- this.state = { ...this.state, stages, status: "running" };
37673
38018
  try {
37674
- const conversationId = await this.deps.driver.newConversation();
37675
- stages = this.state.stages.slice();
37676
- stages[index] = { ...stages[index], conversationId };
37677
- this.state = { ...this.state, stages };
37678
- await this.publish();
37679
- this.deps.driver.mountSkills(stageDef.skillIds);
37680
- const previous = this.previousHandoff(index);
37681
- const prompt = composeStagePrompt(this.pack, index, this.state.task, previous);
37682
- const displayLine = `\u25B6 ${this.pack.name} \u2014 stage ${index + 1}/${this.pack.stages.length}: ${stageDef.name}`;
37683
- let reply = await this.deps.driver.runTurn(prompt, displayLine);
37684
- if (this.control === "abort") {
38019
+ let startSha;
38020
+ let turn;
38021
+ if (inPlace) {
38022
+ startSha = this.state.stages[index].startCommit ?? await this.deps.gates.head();
38023
+ this.patchStage(index, { status: "active", awaitingUser: void 0, error: void 0 });
38024
+ this.state = { ...this.state, status: "running" };
38025
+ await this.publish();
38026
+ turn = { text: "", awaitingUser: false };
38027
+ } else {
38028
+ startSha = await this.deps.gates.head();
38029
+ const prevStage = this.state.stages[index];
38030
+ const attempt = (prevStage.attempts ?? 0) + 1;
38031
+ const previousAttemptError = attempt > 1 ? prevStage.error : void 0;
38032
+ const startCommit = startSha ? await this.deps.gates.canonicalCommit(startSha) : null;
38033
+ this.patchStage(index, {
38034
+ status: "active",
38035
+ attempts: attempt,
38036
+ startCommit: startCommit ?? void 0,
38037
+ conversationId: void 0,
38038
+ handoff: void 0,
38039
+ awaitingUser: void 0,
38040
+ error: void 0
38041
+ });
38042
+ if (index === 0 && !this.state.baseCommit && startCommit) {
38043
+ this.state = { ...this.state, baseCommit: startCommit };
38044
+ }
38045
+ this.state = { ...this.state, status: "running" };
38046
+ const conversationId = await this.deps.driver.newConversation();
38047
+ this.patchStage(index, { conversationId });
38048
+ await this.publish();
38049
+ this.swapSkills(stageDef.skillIds);
38050
+ const prompt = composeStagePrompt(this.pack, index, this.state.task, {
38051
+ baseCommit: this.state.baseCommit,
38052
+ prior: this.priorOutcomes(index),
38053
+ attempt,
38054
+ previousAttemptError
38055
+ });
38056
+ const displayLine = `\u25B6 ${this.pack.name} \u2014 stage ${index + 1}/${this.pack.stages.length}: ${stageDef.name}`;
38057
+ turn = await this.drive(prompt, displayLine);
38058
+ }
38059
+ if (this.aborted()) {
37685
38060
  await this.settle("aborted");
37686
38061
  return false;
37687
38062
  }
38063
+ if (turn.awaitingUser) return this.awaitUser(index);
37688
38064
  const commitOptional = stageDef.requiresCommit === false;
38065
+ let text = turn.text;
37689
38066
  let endSha = await this.deps.gates.head();
37690
- if (!endSha || endSha === startSha) {
37691
- if (commitOptional && reply.trim().length > 0) {
37692
- const reviewedCommit = startSha ? await this.deps.gates.canonicalCommit(startSha) : null;
37693
- const handoff2 = {
37694
- commit: reviewedCommit ?? "(no changes)",
37695
- summary: reply.trim().slice(-SUMMARY_MAX_CHARS),
37696
- diffStat: "reviewed \u2014 no changes needed",
37697
- durationMs: Date.now() - startedMs
37698
- };
37699
- return this.recordHandoff(index, stageDef.role, handoff2);
38067
+ if (!endSha || sameCommit(endSha, startSha)) {
38068
+ if (commitOptional && text.trim().length > 0) {
38069
+ return this.recordHandoff(
38070
+ index,
38071
+ stageDef,
38072
+ await this.noChangeHandoff(startSha, text, startedMs)
38073
+ );
37700
38074
  }
37701
- reply = await this.deps.driver.runTurn(NUDGE_PROMPT, "\u25B6 Waiting for the stage commit\u2026");
38075
+ if (!this.state.stages[index].conversationId) {
38076
+ return this.stall(
38077
+ index,
38078
+ "stage produced no commit \u2014 Retry stage to run it again in a fresh conversation"
38079
+ );
38080
+ }
38081
+ const nudged = await this.drive(NUDGE_PROMPT, "\u25B6 Waiting for the stage commit\u2026");
38082
+ if (this.aborted()) {
38083
+ await this.settle("aborted");
38084
+ return false;
38085
+ }
38086
+ if (nudged.awaitingUser) return this.awaitUser(index);
38087
+ text = nudged.text.trim().length > 0 ? nudged.text : text;
37702
38088
  endSha = await this.deps.gates.head();
37703
- if (!endSha || endSha === startSha) {
37704
- if (commitOptional && reply.trim().length > 0) {
37705
- const reviewedCommit = startSha ? await this.deps.gates.canonicalCommit(startSha) : null;
37706
- return this.recordHandoff(index, stageDef.role, {
37707
- commit: reviewedCommit ?? "(no changes)",
37708
- summary: reply.trim().slice(-SUMMARY_MAX_CHARS),
37709
- diffStat: "reviewed \u2014 no changes needed",
37710
- durationMs: Date.now() - startedMs
37711
- });
38089
+ if (!endSha || sameCommit(endSha, startSha)) {
38090
+ if (commitOptional && text.trim().length > 0) {
38091
+ return this.recordHandoff(
38092
+ index,
38093
+ stageDef,
38094
+ await this.noChangeHandoff(startSha, text, startedMs)
38095
+ );
37712
38096
  }
37713
- return this.stall(index, "stage produced no commit", reply);
38097
+ return this.stall(index, "stage produced no commit", text);
37714
38098
  }
37715
38099
  }
37716
38100
  const commit = await this.deps.gates.canonicalCommit(endSha);
37717
- if (!commit) return this.stall(index, "stage HEAD did not resolve to a commit", reply);
38101
+ if (!commit) return this.stall(index, "stage HEAD did not resolve to a commit", text);
37718
38102
  const handoff = {
37719
38103
  commit,
37720
- summary: reply.trim().slice(-SUMMARY_MAX_CHARS),
38104
+ summary: summarizeReply(text),
37721
38105
  diffStat: startSha ? await this.deps.gates.diffStat(startSha, endSha) : "",
37722
38106
  checks: await this.deps.gates.runChecks() ?? void 0,
37723
38107
  durationMs: Date.now() - startedMs
37724
38108
  };
37725
- return this.recordHandoff(index, stageDef.role, handoff);
38109
+ if (stageDef.producesFindings) {
38110
+ Object.assign(handoff, await this.captureFindings(startSha, endSha));
38111
+ }
38112
+ return this.recordHandoff(index, stageDef, handoff);
37726
38113
  } catch (err) {
37727
- if (this.control === "abort") {
38114
+ if (this.aborted()) {
37728
38115
  await this.settle("aborted");
37729
38116
  return false;
37730
38117
  }
37731
38118
  return this.stall(index, err.message);
37732
38119
  }
37733
38120
  }
38121
+ /** Read through a method: `control` flips from another call while a turn is
38122
+ * awaited, which TS's property narrowing would otherwise hide. */
38123
+ aborted() {
38124
+ return this.control === "abort";
38125
+ }
38126
+ async drive(prompt, displayLine) {
38127
+ this.inTurn = true;
38128
+ try {
38129
+ return await this.deps.driver.runTurn(prompt, displayLine);
38130
+ } finally {
38131
+ this.inTurn = false;
38132
+ }
38133
+ }
38134
+ swapSkills(next) {
38135
+ const stale = this.mountedSkills.filter((id) => !next.includes(id));
38136
+ const fresh = next.filter((id) => !this.mountedSkills.includes(id));
38137
+ if (stale.length > 0) this.deps.driver.unmountSkills(stale);
38138
+ if (fresh.length > 0) this.deps.driver.mountSkills(fresh);
38139
+ this.mountedSkills = next.slice();
38140
+ }
38141
+ async noChangeHandoff(startSha, text, startedMs) {
38142
+ const reviewedCommit = startSha ? await this.deps.gates.canonicalCommit(startSha) : null;
38143
+ return {
38144
+ commit: reviewedCommit ?? "(no changes)",
38145
+ summary: summarizeReply(text),
38146
+ diffStat: "reviewed \u2014 no changes needed",
38147
+ durationMs: Date.now() - startedMs
38148
+ };
38149
+ }
38150
+ /**
38151
+ * Parse the stage's `PACK_FINDINGS_FILE` into the handoff. Only a file the
38152
+ * stage itself touched counts — a stale one left by an earlier run must not
38153
+ * masquerade as this review's verdict. Every failure path leaves an honest
38154
+ * `findingsNote`; none of them stalls the run.
38155
+ */
38156
+ async captureFindings(startSha, endSha) {
38157
+ try {
38158
+ if (startSha) {
38159
+ const changed = await this.deps.gates.changedFiles(startSha, endSha);
38160
+ if (!changed.includes(PACK_FINDINGS_FILE)) {
38161
+ return {
38162
+ findingsNote: `${PACK_FINDINGS_FILE} was not written by this stage \u2014 no structured findings`
38163
+ };
38164
+ }
38165
+ }
38166
+ const raw = await this.deps.gates.readFile(PACK_FINDINGS_FILE);
38167
+ if (raw === null)
38168
+ return { findingsNote: `no ${PACK_FINDINGS_FILE} \u2014 the stage left no structured findings` };
38169
+ const parsed = parsePackFindings(raw);
38170
+ if (!parsed.ok) return { findingsNote: `${PACK_FINDINGS_FILE}: ${parsed.error}` };
38171
+ const notes = [];
38172
+ if (parsed.value.findings.length === 0 && parsed.value.checked) {
38173
+ notes.push(`no findings; checked: ${parsed.value.checked.join(", ")}`);
38174
+ }
38175
+ if (parsed.value.note) notes.push(parsed.value.note);
38176
+ return {
38177
+ findings: parsed.value.findings,
38178
+ ...notes.length > 0 ? { findingsNote: notes.join(" \u2014 ") } : {}
38179
+ };
38180
+ } catch (err) {
38181
+ return { findingsNote: `could not read ${PACK_FINDINGS_FILE}: ${err.message}` };
38182
+ }
38183
+ }
37734
38184
  /** Mark a stage `done` with its handoff, persist, and advance. */
37735
- async recordHandoff(index, role, handoff) {
37736
- const stages = this.state.stages.slice();
37737
- stages[index] = { ...stages[index], status: "done", handoff };
37738
- this.state = { ...this.state, stages, currentStage: index + 1 };
38185
+ async recordHandoff(index, stageDef, handoff) {
38186
+ this.patchStage(index, { status: "done", handoff, awaitingUser: void 0, error: void 0 });
38187
+ this.state = { ...this.state, currentStage: index + 1 };
37739
38188
  try {
37740
- this.deps.ledger.saveStageHandoff(this.state.runId, index, role, handoff);
38189
+ this.deps.ledger.saveStageHandoff(this.state.runId, index, stageDef.role, handoff);
37741
38190
  } catch (err) {
37742
38191
  this.deps.log(`pack handoff save failed: ${err.message}`);
37743
38192
  }
37744
38193
  await this.publish();
37745
38194
  return true;
37746
38195
  }
38196
+ /** The stage asked the user something: park the run, keep the conversation. */
38197
+ async awaitUser(index) {
38198
+ const name = this.state.stages[index].name;
38199
+ this.patchStage(index, { awaitingUser: true });
38200
+ this.control = "pause";
38201
+ await this.settle(
38202
+ "paused",
38203
+ `${name} asked you a question \u2014 answer it in the stage chat, then tap Resume.`
38204
+ );
38205
+ this.deps.log(
38206
+ `pack run ${this.state.runId} paused at stage ${index + 1}: awaiting the user's answer`
38207
+ );
38208
+ return false;
38209
+ }
37747
38210
  async stall(index, reason, lastReply) {
37748
- const stages = this.state.stages.slice();
37749
- stages[index] = { ...stages[index], status: "failed", error: reason };
37750
- this.state = { ...this.state, stages };
37751
- await this.settle("stalled", lastReply ? `${reason} \u2014 last reply: ${lastReply.slice(-300)}` : reason);
38211
+ this.patchStage(index, { status: "failed", error: reason, awaitingUser: void 0 });
38212
+ await this.settle(
38213
+ "stalled",
38214
+ lastReply ? `${reason} \u2014 last reply: ${lastReply.slice(-300)}` : reason
38215
+ );
37752
38216
  this.deps.log(`pack run ${this.state.runId} stalled at stage ${index + 1}: ${reason}`);
37753
38217
  return false;
37754
38218
  }
37755
- previousHandoff(index) {
37756
- for (let i = index - 1; i >= 0; i--) {
38219
+ priorOutcomes(index) {
38220
+ const out2 = [];
38221
+ for (let i = 0; i < index; i++) {
37757
38222
  const s = this.state.stages[i];
37758
- if (s.status === "done" && s.handoff) return { role: s.role, handoff: s.handoff };
38223
+ if (s.status === "done" && s.handoff)
38224
+ out2.push({ role: s.role, name: s.name, status: "done", handoff: s.handoff });
38225
+ else if (s.status === "skipped") out2.push({ role: s.role, name: s.name, status: "skipped" });
37759
38226
  }
37760
- return null;
38227
+ return out2;
37761
38228
  }
37762
38229
  };
37763
38230
 
@@ -37771,7 +38238,7 @@ function getActivePackRunner() {
37771
38238
  }
37772
38239
 
37773
38240
  // src/packs/handlers.ts
37774
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "aborted", "failed"]);
38241
+ var TERMINAL_STATUSES = TERMINAL_PACK_STATUSES;
37775
38242
  var PACK_ACTIONS = /* @__PURE__ */ new Set(["pause", "resume", "retry_stage", "skip_stage", "abort"]);
37776
38243
  function buildPackRunnerDeps(ctx) {
37777
38244
  const cwd = ctx.opts.cwd;
@@ -37791,10 +38258,10 @@ function buildPackRunnerDeps(ctx) {
37791
38258
  ctx.history.appendUserPrompt(displayLine);
37792
38259
  await ctx.client.prompt(prompt);
37793
38260
  const text = ctx.streaming.getCurrentText();
37794
- await ctx.streaming.closeTurnWithInteractiveDetection();
38261
+ const awaitingUser = await ctx.streaming.closeTurnWithInteractiveDetection();
37795
38262
  ctx.history.appendAgentReply(text);
37796
38263
  await ctx.history.flush();
37797
- return text;
38264
+ return { text, awaitingUser };
37798
38265
  },
37799
38266
  cancel: () => ctx.client.cancel(),
37800
38267
  mountSkills: (skillIds) => {
@@ -37805,16 +38272,27 @@ function buildPackRunnerDeps(ctx) {
37805
38272
  log.warn("packs", `skill mount failed for ${id}: ${err.message}`);
37806
38273
  }
37807
38274
  }
38275
+ },
38276
+ unmountSkills: (skillIds) => {
38277
+ for (const id of skillIds) {
38278
+ try {
38279
+ configureSkill("remove", id);
38280
+ } catch (err) {
38281
+ log.warn("packs", `skill unmount failed for ${id}: ${err.message}`);
38282
+ }
38283
+ }
37808
38284
  }
37809
38285
  },
37810
38286
  gates: {
37811
38287
  head: () => gitHead(run, cwd),
37812
38288
  canonicalCommit: (sha) => canonicalCommit(run, cwd, sha),
37813
38289
  diffStat: (from, to) => diffStat(run, cwd, from, to),
38290
+ changedFiles: (from, to) => changedFiles(run, cwd, from, to),
37814
38291
  runChecks: async () => {
37815
38292
  const command2 = detectChecksCommand(cwd);
37816
38293
  return command2 ? runChecks(run, cwd, command2) : null;
37817
- }
38294
+ },
38295
+ readFile: async (relPath) => readWorkspaceFile(cwd, relPath)
37818
38296
  },
37819
38297
  ledger: {
37820
38298
  saveRun: (state) => saveRun(cwd, state),
@@ -37832,19 +38310,38 @@ function buildPackRunnerDeps(ctx) {
37832
38310
  log: (message) => log.info("packs", message)
37833
38311
  };
37834
38312
  }
38313
+ async function ensureRunner(ctx) {
38314
+ const active2 = getActivePackRunner();
38315
+ if (active2) return active2;
38316
+ const stored = loadLatestRun(ctx.opts.cwd);
38317
+ if (!stored || TERMINAL_STATUSES.has(stored.status)) return null;
38318
+ const runner = PackRunner.rehydrate(buildPackRunnerDeps(ctx), stored);
38319
+ if (!runner) return null;
38320
+ setActivePackRunner(runner);
38321
+ log.info(
38322
+ "packs",
38323
+ `rehydrated run ${stored.runId} from the ledger (was ${stored.status}) \u2192 ${runner.getState().status}`
38324
+ );
38325
+ await runner.announce();
38326
+ return runner;
38327
+ }
37835
38328
  var packStartH = async (ctx) => {
37836
38329
  const payload = ctx.cmd.payload;
37837
38330
  const packId = typeof payload?.packId === "string" ? payload.packId : "";
37838
38331
  const task = typeof payload?.task === "string" ? payload.task.trim() : "";
37839
38332
  if (!isPackId(packId)) {
37840
- await ctx.relay.sendResult(ctx.cmd.id, "failed", { error: `unknown pack: ${packId || "(none)"}` });
38333
+ await ctx.relay.sendResult(ctx.cmd.id, "failed", {
38334
+ error: `unknown pack: ${packId || "(none)"}`
38335
+ });
37841
38336
  return;
37842
38337
  }
37843
38338
  if (task.length === 0) {
37844
- await ctx.relay.sendResult(ctx.cmd.id, "failed", { error: "pack_start requires a non-empty task" });
38339
+ await ctx.relay.sendResult(ctx.cmd.id, "failed", {
38340
+ error: "pack_start requires a non-empty task"
38341
+ });
37845
38342
  return;
37846
38343
  }
37847
- const existing = getActivePackRunner();
38344
+ const existing = await ensureRunner(ctx);
37848
38345
  if (existing && !TERMINAL_STATUSES.has(existing.getState().status)) {
37849
38346
  await ctx.relay.sendResult(ctx.cmd.id, "failed", {
37850
38347
  error: "a pack run is already active on this session \u2014 pause/abort it first",
@@ -37867,10 +38364,12 @@ var packActionH = async (ctx) => {
37867
38364
  const payload = ctx.cmd.payload;
37868
38365
  const action = typeof payload?.action === "string" ? payload.action : "";
37869
38366
  if (!PACK_ACTIONS.has(action)) {
37870
- await ctx.relay.sendResult(ctx.cmd.id, "failed", { error: `unknown pack action: ${action || "(none)"}` });
38367
+ await ctx.relay.sendResult(ctx.cmd.id, "failed", {
38368
+ error: `unknown pack action: ${action || "(none)"}`
38369
+ });
37871
38370
  return;
37872
38371
  }
37873
- const runner = getActivePackRunner();
38372
+ const runner = await ensureRunner(ctx);
37874
38373
  if (!runner) {
37875
38374
  await ctx.relay.sendResult(ctx.cmd.id, "failed", {
37876
38375
  error: "no active pack run in this session",
@@ -37878,11 +38377,20 @@ var packActionH = async (ctx) => {
37878
38377
  });
37879
38378
  return;
37880
38379
  }
37881
- const state = await runner.applyAction(action);
37882
- await ctx.relay.sendResult(ctx.cmd.id, "completed", { state });
38380
+ const outcome = await runner.applyAction(action);
38381
+ if (outcome.rejected) {
38382
+ log.info("packs", `pack_action ${action} rejected: ${outcome.rejected}`);
38383
+ await ctx.relay.sendResult(ctx.cmd.id, "failed", {
38384
+ error: outcome.rejected,
38385
+ state: outcome.state
38386
+ });
38387
+ return;
38388
+ }
38389
+ await ctx.relay.sendResult(ctx.cmd.id, "completed", { state: outcome.state });
37883
38390
  };
37884
38391
  var packStatusH = async (ctx) => {
37885
- const state = getActivePackRunner()?.getState() ?? loadLatestRun(ctx.opts.cwd);
38392
+ const runner = await ensureRunner(ctx);
38393
+ const state = runner?.getState() ?? loadLatestRun(ctx.opts.cwd);
37886
38394
  await ctx.relay.sendResult(ctx.cmd.id, "completed", { state });
37887
38395
  };
37888
38396
 
@@ -46506,7 +47014,7 @@ async function autoUpgradeBeforeCriticalCommand() {
46506
47014
  if (process.env.NODE_ENV === "test") return;
46507
47015
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
46508
47016
  if (process.env.CI) return;
46509
- const current2 = true ? "2.75.8" : null;
47017
+ const current2 = true ? "2.75.9" : null;
46510
47018
  if (!current2) return;
46511
47019
  const cache = readCache();
46512
47020
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -46523,7 +47031,7 @@ function checkForUpdates() {
46523
47031
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
46524
47032
  if (process.env.CI) return;
46525
47033
  if (!process.stdout.isTTY) return;
46526
- const current2 = true ? "2.75.8" : null;
47034
+ const current2 = true ? "2.75.9" : null;
46527
47035
  if (!current2) return;
46528
47036
  const cache = readCache();
46529
47037
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -46546,7 +47054,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
46546
47054
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
46547
47055
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
46548
47056
  function currentCliVersion() {
46549
- return true ? "2.75.8" : null;
47057
+ return true ? "2.75.9" : null;
46550
47058
  }
46551
47059
  async function installedVersion(deps) {
46552
47060
  const ls = await deps.run(
@@ -48286,7 +48794,7 @@ function checkChokidar() {
48286
48794
  }
48287
48795
  async function doctor(args2 = []) {
48288
48796
  const json = args2.includes("--json");
48289
- const cliVersion = true ? "2.75.8" : "0.0.0-dev";
48797
+ const cliVersion = true ? "2.75.9" : "0.0.0-dev";
48290
48798
  const apiBase2 = resolveApiBaseUrl();
48291
48799
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
48292
48800
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -48810,7 +49318,7 @@ async function mcpWarm(argv = [], opts = {}) {
48810
49318
  // src/commands/version.ts
48811
49319
  var import_picocolors15 = __toESM(require("picocolors"));
48812
49320
  function version2() {
48813
- const v = true ? "2.75.8" : "unknown";
49321
+ const v = true ? "2.75.9" : "unknown";
48814
49322
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
48815
49323
  }
48816
49324
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.75.8",
3
+ "version": "2.75.9",
4
4
  "description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",