codeam-cli 2.75.7 → 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.
- package/CHANGELOG.md +13 -0
- package/dist/index.js +741 -149
- package/package.json +1 -1
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.
|
|
2909
|
-
|
|
2910
|
-
|
|
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.
|
|
2918
|
-
3.
|
|
2919
|
-
4.
|
|
2920
|
-
5.
|
|
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
|
|
3008
|
-
- **If you
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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,6 +11549,9 @@ var streamHost = new StreamHostSelector(
|
|
|
11384
11549
|
);
|
|
11385
11550
|
|
|
11386
11551
|
// src/services/command-relay.service.ts
|
|
11552
|
+
function isRecord3(v) {
|
|
11553
|
+
return typeof v === "object" && v !== null;
|
|
11554
|
+
}
|
|
11387
11555
|
function httpStatusOf(err) {
|
|
11388
11556
|
if (typeof err === "object" && err !== null && "statusCode" in err) {
|
|
11389
11557
|
const status2 = err.statusCode;
|
|
@@ -11394,6 +11562,8 @@ function httpStatusOf(err) {
|
|
|
11394
11562
|
var API_BASE2 = resolveApiBaseUrl();
|
|
11395
11563
|
var PENDING_STREAM_PATH = "/api/commands/pending/stream";
|
|
11396
11564
|
var SSE_LIVENESS_TIMEOUT_MS = 45e3;
|
|
11565
|
+
var HEARTBEAT_RETRY_MS = 3e3;
|
|
11566
|
+
var HEARTBEAT_WARN_INTERVAL_MS = 6e4;
|
|
11397
11567
|
var SSE_WATCHDOG_INTERVAL_MS = 1e4;
|
|
11398
11568
|
var CommandRelayService = class _CommandRelayService {
|
|
11399
11569
|
constructor(pluginId, onCommand, agentMeta, agentsOverride, explicitPollSecret, onHeartbeat) {
|
|
@@ -11412,6 +11582,18 @@ var CommandRelayService = class _CommandRelayService {
|
|
|
11412
11582
|
onHeartbeat;
|
|
11413
11583
|
_running = false;
|
|
11414
11584
|
pairingInvalid = false;
|
|
11585
|
+
/**
|
|
11586
|
+
* Heartbeat health. Before 2026-09-23 a failed heartbeat was a `log.trace`,
|
|
11587
|
+
* i.e. INVISIBLE in a normal `~/.codeam/debug-*.log` — a user whose beats
|
|
11588
|
+
* never reached the origin (every other request did) spent a day on "Server
|
|
11589
|
+
* Failed to Start" with nothing in his log to show for it. Failures are now
|
|
11590
|
+
* WARN (rate-limited per streak) + a PostHog event, and the process exits
|
|
11591
|
+
* when the backend answers `paired:false`.
|
|
11592
|
+
*/
|
|
11593
|
+
heartbeatFailStreak = 0;
|
|
11594
|
+
heartbeatLastWarnAt = 0;
|
|
11595
|
+
sessionGone = false;
|
|
11596
|
+
onSessionGone;
|
|
11415
11597
|
heartbeatTimer = null;
|
|
11416
11598
|
agentsTimer = null;
|
|
11417
11599
|
/** Injectable for tests; defaults to real fs/fetch/spawn wiring. */
|
|
@@ -11530,6 +11712,17 @@ var CommandRelayService = class _CommandRelayService {
|
|
|
11530
11712
|
stop() {
|
|
11531
11713
|
void this.stopAndFlush();
|
|
11532
11714
|
}
|
|
11715
|
+
/**
|
|
11716
|
+
* Called ONCE when a heartbeat answers `paired:false` — the backend no longer
|
|
11717
|
+
* has a live session for this pluginId (deleted / disconnected from the
|
|
11718
|
+
* app). The relay has already stopped itself by then; the owner should exit
|
|
11719
|
+
* the process like a Ctrl-C would. Backstop for a process whose
|
|
11720
|
+
* `session_terminated` command was consumed by a sibling on the same
|
|
11721
|
+
* pluginId (two `codeam start` on one saved session, 2026-09-23).
|
|
11722
|
+
*/
|
|
11723
|
+
setOnSessionGone(cb) {
|
|
11724
|
+
this.onSessionGone = cb;
|
|
11725
|
+
}
|
|
11533
11726
|
/**
|
|
11534
11727
|
* Same teardown as {@link stop}, but resolves only once the `online:false`
|
|
11535
11728
|
* heartbeat POST has settled — so a caller can `await` it before exiting.
|
|
@@ -11877,8 +12070,9 @@ var CommandRelayService = class _CommandRelayService {
|
|
|
11877
12070
|
});
|
|
11878
12071
|
}
|
|
11879
12072
|
// ─── Heartbeat + agents ──────────────────────────────────────────
|
|
11880
|
-
async sendHeartbeat(online) {
|
|
11881
|
-
|
|
12073
|
+
async sendHeartbeat(online, attempt = 0) {
|
|
12074
|
+
if (this.sessionGone && online) return;
|
|
12075
|
+
const body = {
|
|
11882
12076
|
pluginId: this.pluginId,
|
|
11883
12077
|
online,
|
|
11884
12078
|
agentId: this.agentMeta.id,
|
|
@@ -11887,8 +12081,58 @@ var CommandRelayService = class _CommandRelayService {
|
|
|
11887
12081
|
// fresh + clear the "CLI update available" banner after a self-update
|
|
11888
12082
|
// (a codespace that reinstalls @latest reconnects via heartbeat, not
|
|
11889
12083
|
// pair/reconnect). Older backends ignore the extra field.
|
|
11890
|
-
..."2.75.
|
|
11891
|
-
}
|
|
12084
|
+
..."2.75.9" ? { ideVersion: "2.75.9" } : {}
|
|
12085
|
+
};
|
|
12086
|
+
try {
|
|
12087
|
+
const res = await _postJson(`${API_BASE2}/api/plugin/heartbeat`, body);
|
|
12088
|
+
this.heartbeatFailStreak = 0;
|
|
12089
|
+
log.trace("relay", `heartbeat ok online=${online}`);
|
|
12090
|
+
const data = res?.data;
|
|
12091
|
+
if (online && isRecord3(data) && data.paired === false) this.handleSessionGone();
|
|
12092
|
+
} catch (err) {
|
|
12093
|
+
if (!online) {
|
|
12094
|
+
log.trace("relay", "goodbye heartbeat failed", err);
|
|
12095
|
+
return;
|
|
12096
|
+
}
|
|
12097
|
+
this.heartbeatFailStreak += 1;
|
|
12098
|
+
this.noteHeartbeatFailure(err);
|
|
12099
|
+
if (attempt === 0 && this._running) {
|
|
12100
|
+
const t2 = setTimeout(() => {
|
|
12101
|
+
void this.sendHeartbeat(true, 1);
|
|
12102
|
+
}, HEARTBEAT_RETRY_MS);
|
|
12103
|
+
t2.unref?.();
|
|
12104
|
+
}
|
|
12105
|
+
}
|
|
12106
|
+
}
|
|
12107
|
+
noteHeartbeatFailure(err) {
|
|
12108
|
+
const status2 = httpStatusOf(err);
|
|
12109
|
+
const code = err?.code;
|
|
12110
|
+
const reason = status2 ? `status_${status2}` : typeof code === "string" ? code : (err instanceof Error ? err.message : String(err)).slice(0, 80);
|
|
12111
|
+
const now = Date.now();
|
|
12112
|
+
const firstOfStreak = this.heartbeatFailStreak === 1;
|
|
12113
|
+
if (!firstOfStreak && now - this.heartbeatLastWarnAt < HEARTBEAT_WARN_INTERVAL_MS) return;
|
|
12114
|
+
this.heartbeatLastWarnAt = now;
|
|
12115
|
+
log.warn(
|
|
12116
|
+
"relay",
|
|
12117
|
+
`heartbeat failed (${reason}, streak=${this.heartbeatFailStreak}) \u2014 two misses in a row and the app shows this session OFFLINE even though it is running`
|
|
12118
|
+
);
|
|
12119
|
+
capture("heartbeat_failed", {
|
|
12120
|
+
pluginId: this.pluginId,
|
|
12121
|
+
agentId: this.agentMeta.id,
|
|
12122
|
+
reason,
|
|
12123
|
+
streak: this.heartbeatFailStreak
|
|
12124
|
+
});
|
|
12125
|
+
}
|
|
12126
|
+
handleSessionGone() {
|
|
12127
|
+
if (this.sessionGone) return;
|
|
12128
|
+
log.warn("relay", "backend reports this session no longer exists (paired:false) \u2014 stopping");
|
|
12129
|
+
capture("session_gone_detected", { pluginId: this.pluginId, agentId: this.agentMeta.id });
|
|
12130
|
+
process.stderr.write(
|
|
12131
|
+
"[codeam] This session was deleted or disconnected from the app \u2014 exiting. Run `codeam pair` to start a new one.\n"
|
|
12132
|
+
);
|
|
12133
|
+
this.stop();
|
|
12134
|
+
this.sessionGone = true;
|
|
12135
|
+
this.onSessionGone?.();
|
|
11892
12136
|
}
|
|
11893
12137
|
/**
|
|
11894
12138
|
* Refresh {@link cachedBranch} without blocking the event loop.
|
|
@@ -31218,14 +31462,14 @@ var zGuardCreateElicitationResponseCancel = z7.object({
|
|
|
31218
31462
|
|
|
31219
31463
|
// ../../node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js
|
|
31220
31464
|
var CANCEL_REQUEST_METHOD = "$/cancel_request";
|
|
31221
|
-
function
|
|
31465
|
+
function isRecord4(value) {
|
|
31222
31466
|
return typeof value === "object" && value !== null;
|
|
31223
31467
|
}
|
|
31224
31468
|
function isJsonRpcId(value) {
|
|
31225
31469
|
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
31226
31470
|
}
|
|
31227
31471
|
function cancelRequestId(params) {
|
|
31228
|
-
if (!
|
|
31472
|
+
if (!isRecord4(params) || !isJsonRpcId(params["requestId"])) {
|
|
31229
31473
|
return void 0;
|
|
31230
31474
|
}
|
|
31231
31475
|
return params["requestId"];
|
|
@@ -31657,7 +31901,7 @@ var Connection = class {
|
|
|
31657
31901
|
if (this.abortController.signal.aborted) {
|
|
31658
31902
|
return;
|
|
31659
31903
|
}
|
|
31660
|
-
if (!
|
|
31904
|
+
if (!isRecord4(message)) {
|
|
31661
31905
|
console.error("Invalid message", { message });
|
|
31662
31906
|
return;
|
|
31663
31907
|
}
|
|
@@ -31750,7 +31994,7 @@ var Connection = class {
|
|
|
31750
31994
|
pendingResponse.cleanup?.();
|
|
31751
31995
|
if ("result" in response) {
|
|
31752
31996
|
pendingResponse.resolve(response.result);
|
|
31753
|
-
} else if ("error" in response &&
|
|
31997
|
+
} else if ("error" in response && isRecord4(response.error)) {
|
|
31754
31998
|
const { code, message, data } = response.error;
|
|
31755
31999
|
pendingResponse.reject(new RequestError(code, message, data));
|
|
31756
32000
|
} else {
|
|
@@ -32023,7 +32267,7 @@ function ndJsonStream(output, input) {
|
|
|
32023
32267
|
if (trimmedLine) {
|
|
32024
32268
|
try {
|
|
32025
32269
|
const message = JSON.parse(trimmedLine);
|
|
32026
|
-
if (
|
|
32270
|
+
if (isRecord4(message)) {
|
|
32027
32271
|
controller.enqueue(message);
|
|
32028
32272
|
} else {
|
|
32029
32273
|
console.warn("Skipping JSON line that is not an object:", trimmedLine);
|
|
@@ -37277,11 +37521,16 @@ async function postCredentialSync(opts, fetchImpl = fetch) {
|
|
|
37277
37521
|
var import_node_child_process22 = require("child_process");
|
|
37278
37522
|
var fs60 = __toESM(require("fs"));
|
|
37279
37523
|
var path70 = __toESM(require("path"));
|
|
37280
|
-
var defaultCommandRunner = (file, args2, cwd, timeoutMs) => new Promise((resolve11) => {
|
|
37524
|
+
var defaultCommandRunner = (file, args2, cwd, timeoutMs, env) => new Promise((resolve11) => {
|
|
37281
37525
|
(0, import_node_child_process22.execFile)(
|
|
37282
37526
|
file,
|
|
37283
37527
|
args2,
|
|
37284
|
-
{
|
|
37528
|
+
{
|
|
37529
|
+
cwd,
|
|
37530
|
+
timeout: timeoutMs,
|
|
37531
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
37532
|
+
env: env ? { ...process.env, ...env } : process.env
|
|
37533
|
+
},
|
|
37285
37534
|
(err, stdout, stderr) => {
|
|
37286
37535
|
let code = 0;
|
|
37287
37536
|
if (err) {
|
|
@@ -37298,7 +37547,12 @@ async function gitHead(run, cwd) {
|
|
|
37298
37547
|
return res.code === 0 ? res.stdout.trim() : null;
|
|
37299
37548
|
}
|
|
37300
37549
|
async function canonicalCommit(run, cwd, sha) {
|
|
37301
|
-
const verify = await run(
|
|
37550
|
+
const verify = await run(
|
|
37551
|
+
"git",
|
|
37552
|
+
["rev-parse", "--verify", `${sha}^{commit}`],
|
|
37553
|
+
cwd,
|
|
37554
|
+
GIT_TIMEOUT_MS
|
|
37555
|
+
);
|
|
37302
37556
|
if (verify.code !== 0) return null;
|
|
37303
37557
|
const short = await run("git", ["rev-parse", "--short=10", sha], cwd, GIT_TIMEOUT_MS);
|
|
37304
37558
|
return short.code === 0 ? short.stdout.trim() : null;
|
|
@@ -37309,6 +37563,18 @@ async function diffStat(run, cwd, from, to) {
|
|
|
37309
37563
|
const lines = res.stdout.trim().split("\n").filter(Boolean);
|
|
37310
37564
|
return lines.length > 0 ? lines[lines.length - 1].trim() : "";
|
|
37311
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
|
+
}
|
|
37312
37578
|
var NO_TEST_PLACEHOLDER = 'echo "Error: no test specified"';
|
|
37313
37579
|
var CHECKS_TIMEOUT_MS = 5 * 6e4;
|
|
37314
37580
|
function detectChecksCommand(cwd) {
|
|
@@ -37330,7 +37596,7 @@ function detectChecksCommand(cwd) {
|
|
|
37330
37596
|
return null;
|
|
37331
37597
|
}
|
|
37332
37598
|
async function runChecks(run, cwd, command2) {
|
|
37333
|
-
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" });
|
|
37334
37600
|
const combined = `${res.stdout}
|
|
37335
37601
|
${res.stderr}`.trim();
|
|
37336
37602
|
const tail = combined.split("\n").slice(-12).join("\n").slice(-1500);
|
|
@@ -37427,33 +37693,109 @@ async function postPackState(opts, state, fetchImpl = fetch) {
|
|
|
37427
37693
|
}
|
|
37428
37694
|
|
|
37429
37695
|
// src/packs/runner.ts
|
|
37430
|
-
var
|
|
37431
|
-
|
|
37432
|
-
|
|
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) {
|
|
37433
37706
|
const stage = pack.stages[stageIndex];
|
|
37434
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
|
+
}
|
|
37435
37716
|
const parts = [
|
|
37436
37717
|
stage.prompt,
|
|
37437
37718
|
PACK_WORKFLOW_ARTICLE,
|
|
37438
37719
|
`## Your pipeline position
|
|
37439
|
-
${
|
|
37720
|
+
${position.join("\n")}`,
|
|
37440
37721
|
`## Task
|
|
37441
37722
|
${task}`
|
|
37442
37723
|
];
|
|
37443
|
-
|
|
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
|
+
}
|
|
37444
37735
|
parts.push(
|
|
37445
|
-
`## Previous stage handoff (${
|
|
37446
|
-
|
|
37447
|
-
|
|
37448
|
-
|
|
37449
|
-
|
|
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.`
|
|
37450
37751
|
);
|
|
37451
37752
|
}
|
|
37452
37753
|
return parts.join("\n\n");
|
|
37453
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
|
+
}
|
|
37454
37792
|
function nowIso() {
|
|
37455
37793
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
37456
37794
|
}
|
|
37795
|
+
function sameCommit(a, b) {
|
|
37796
|
+
if (!a || !b) return a === b;
|
|
37797
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
37798
|
+
}
|
|
37457
37799
|
var PackRunner = class _PackRunner {
|
|
37458
37800
|
constructor(deps, pack, initial) {
|
|
37459
37801
|
this.deps = deps;
|
|
@@ -37465,6 +37807,10 @@ var PackRunner = class _PackRunner {
|
|
|
37465
37807
|
state;
|
|
37466
37808
|
control = "run";
|
|
37467
37809
|
looping = false;
|
|
37810
|
+
inTurn = false;
|
|
37811
|
+
/** Next `runStage` continues the current stage in place (no new prompt). */
|
|
37812
|
+
resumeInPlace = false;
|
|
37813
|
+
mountedSkills = [];
|
|
37468
37814
|
static create(deps, packId, task, runId) {
|
|
37469
37815
|
const pack = getPackDefinition(packId);
|
|
37470
37816
|
if (!pack) throw new Error(`unknown pack: ${packId}`);
|
|
@@ -37485,9 +37831,51 @@ var PackRunner = class _PackRunner {
|
|
|
37485
37831
|
};
|
|
37486
37832
|
return new _PackRunner(deps, pack, state);
|
|
37487
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
|
+
}
|
|
37488
37872
|
getState() {
|
|
37489
37873
|
return this.state;
|
|
37490
37874
|
}
|
|
37875
|
+
/** Persist + post the current state without changing it (rehydration announce). */
|
|
37876
|
+
async announce() {
|
|
37877
|
+
await this.publish();
|
|
37878
|
+
}
|
|
37491
37879
|
/** Persist + post the current state (ledger first — it's the truth). */
|
|
37492
37880
|
async publish() {
|
|
37493
37881
|
this.state = { ...this.state, updatedAt: nowIso() };
|
|
@@ -37499,64 +37887,101 @@ var PackRunner = class _PackRunner {
|
|
|
37499
37887
|
await this.deps.postState(this.state);
|
|
37500
37888
|
}
|
|
37501
37889
|
async settle(status2, stalledReason) {
|
|
37502
|
-
this.state = { ...this.state, status: status2, stalledReason };
|
|
37890
|
+
this.state = { ...this.state, status: status2, stalledReason, pendingControl: void 0 };
|
|
37503
37891
|
await this.publish();
|
|
37504
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
|
+
}
|
|
37505
37898
|
// ── Control surface (relay `pack_action`) ────────────────────────────────
|
|
37506
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})`);
|
|
37507
37904
|
switch (action) {
|
|
37508
|
-
case "pause":
|
|
37905
|
+
case "pause": {
|
|
37906
|
+
if (status2 === "paused" || status2 === "stalled") return { state: this.state };
|
|
37509
37907
|
this.control = "pause";
|
|
37510
|
-
|
|
37511
|
-
|
|
37512
|
-
await this.publish();
|
|
37513
|
-
}
|
|
37908
|
+
this.state = { ...this.state, pendingControl: "pause" };
|
|
37909
|
+
await this.publish();
|
|
37514
37910
|
break;
|
|
37515
|
-
|
|
37911
|
+
}
|
|
37912
|
+
case "resume": {
|
|
37913
|
+
if (status2 !== "paused" && status2 !== "stalled")
|
|
37914
|
+
return reject(`nothing to resume \u2014 the run is ${status2}`);
|
|
37516
37915
|
this.control = "run";
|
|
37517
|
-
|
|
37518
|
-
|
|
37519
|
-
|
|
37520
|
-
|
|
37521
|
-
|
|
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();
|
|
37522
37926
|
break;
|
|
37927
|
+
}
|
|
37523
37928
|
case "retry_stage": {
|
|
37524
|
-
|
|
37525
|
-
|
|
37526
|
-
|
|
37527
|
-
|
|
37528
|
-
|
|
37529
|
-
|
|
37530
|
-
|
|
37531
|
-
void
|
|
37532
|
-
|
|
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();
|
|
37533
37952
|
break;
|
|
37534
37953
|
}
|
|
37535
37954
|
case "skip_stage": {
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
37539
|
-
|
|
37540
|
-
|
|
37541
|
-
|
|
37542
|
-
|
|
37543
|
-
|
|
37544
|
-
|
|
37545
|
-
|
|
37546
|
-
|
|
37547
|
-
|
|
37548
|
-
|
|
37549
|
-
|
|
37550
|
-
|
|
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();
|
|
37551
37970
|
break;
|
|
37552
37971
|
}
|
|
37553
|
-
case "abort":
|
|
37972
|
+
case "abort": {
|
|
37554
37973
|
this.control = "abort";
|
|
37555
|
-
|
|
37556
|
-
|
|
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
|
+
}
|
|
37557
37981
|
break;
|
|
37982
|
+
}
|
|
37558
37983
|
}
|
|
37559
|
-
return this.state;
|
|
37984
|
+
return { state: this.state };
|
|
37560
37985
|
}
|
|
37561
37986
|
// ── The loop ──────────────────────────────────────────────────────────────
|
|
37562
37987
|
async run() {
|
|
@@ -37581,104 +38006,225 @@ var PackRunner = class _PackRunner {
|
|
|
37581
38006
|
await this.settle("failed", err.message).catch(() => void 0);
|
|
37582
38007
|
} finally {
|
|
37583
38008
|
this.looping = false;
|
|
38009
|
+
this.inTurn = false;
|
|
37584
38010
|
}
|
|
37585
38011
|
}
|
|
37586
38012
|
/** Run one stage to its handoff. True = advanced; false = run settled. */
|
|
37587
38013
|
async runStage(index) {
|
|
37588
38014
|
const stageDef = this.pack.stages[index];
|
|
38015
|
+
const inPlace = this.resumeInPlace;
|
|
38016
|
+
this.resumeInPlace = false;
|
|
37589
38017
|
const startedMs = Date.now();
|
|
37590
|
-
const startSha = await this.deps.gates.head();
|
|
37591
|
-
let stages = this.state.stages.slice();
|
|
37592
|
-
stages[index] = { ...stages[index], status: "active" };
|
|
37593
|
-
this.state = { ...this.state, stages, status: "running" };
|
|
37594
38018
|
try {
|
|
37595
|
-
|
|
37596
|
-
|
|
37597
|
-
|
|
37598
|
-
|
|
37599
|
-
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
|
|
37605
|
-
|
|
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()) {
|
|
37606
38060
|
await this.settle("aborted");
|
|
37607
38061
|
return false;
|
|
37608
38062
|
}
|
|
38063
|
+
if (turn.awaitingUser) return this.awaitUser(index);
|
|
37609
38064
|
const commitOptional = stageDef.requiresCommit === false;
|
|
38065
|
+
let text = turn.text;
|
|
37610
38066
|
let endSha = await this.deps.gates.head();
|
|
37611
|
-
if (!endSha || endSha
|
|
37612
|
-
if (commitOptional &&
|
|
37613
|
-
|
|
37614
|
-
|
|
37615
|
-
|
|
37616
|
-
|
|
37617
|
-
|
|
37618
|
-
durationMs: Date.now() - startedMs
|
|
37619
|
-
};
|
|
37620
|
-
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
|
+
);
|
|
37621
38074
|
}
|
|
37622
|
-
|
|
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;
|
|
37623
38088
|
endSha = await this.deps.gates.head();
|
|
37624
|
-
if (!endSha || endSha
|
|
37625
|
-
if (commitOptional &&
|
|
37626
|
-
|
|
37627
|
-
|
|
37628
|
-
|
|
37629
|
-
|
|
37630
|
-
|
|
37631
|
-
durationMs: Date.now() - startedMs
|
|
37632
|
-
});
|
|
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
|
+
);
|
|
37633
38096
|
}
|
|
37634
|
-
return this.stall(index, "stage produced no commit",
|
|
38097
|
+
return this.stall(index, "stage produced no commit", text);
|
|
37635
38098
|
}
|
|
37636
38099
|
}
|
|
37637
38100
|
const commit = await this.deps.gates.canonicalCommit(endSha);
|
|
37638
|
-
if (!commit) return this.stall(index, "stage HEAD did not resolve to a commit",
|
|
38101
|
+
if (!commit) return this.stall(index, "stage HEAD did not resolve to a commit", text);
|
|
37639
38102
|
const handoff = {
|
|
37640
38103
|
commit,
|
|
37641
|
-
summary:
|
|
38104
|
+
summary: summarizeReply(text),
|
|
37642
38105
|
diffStat: startSha ? await this.deps.gates.diffStat(startSha, endSha) : "",
|
|
37643
38106
|
checks: await this.deps.gates.runChecks() ?? void 0,
|
|
37644
38107
|
durationMs: Date.now() - startedMs
|
|
37645
38108
|
};
|
|
37646
|
-
|
|
38109
|
+
if (stageDef.producesFindings) {
|
|
38110
|
+
Object.assign(handoff, await this.captureFindings(startSha, endSha));
|
|
38111
|
+
}
|
|
38112
|
+
return this.recordHandoff(index, stageDef, handoff);
|
|
37647
38113
|
} catch (err) {
|
|
37648
|
-
if (this.
|
|
38114
|
+
if (this.aborted()) {
|
|
37649
38115
|
await this.settle("aborted");
|
|
37650
38116
|
return false;
|
|
37651
38117
|
}
|
|
37652
38118
|
return this.stall(index, err.message);
|
|
37653
38119
|
}
|
|
37654
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
|
+
}
|
|
37655
38184
|
/** Mark a stage `done` with its handoff, persist, and advance. */
|
|
37656
|
-
async recordHandoff(index,
|
|
37657
|
-
|
|
37658
|
-
|
|
37659
|
-
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 };
|
|
37660
38188
|
try {
|
|
37661
|
-
this.deps.ledger.saveStageHandoff(this.state.runId, index, role, handoff);
|
|
38189
|
+
this.deps.ledger.saveStageHandoff(this.state.runId, index, stageDef.role, handoff);
|
|
37662
38190
|
} catch (err) {
|
|
37663
38191
|
this.deps.log(`pack handoff save failed: ${err.message}`);
|
|
37664
38192
|
}
|
|
37665
38193
|
await this.publish();
|
|
37666
38194
|
return true;
|
|
37667
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
|
+
}
|
|
37668
38210
|
async stall(index, reason, lastReply) {
|
|
37669
|
-
|
|
37670
|
-
|
|
37671
|
-
|
|
37672
|
-
|
|
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
|
+
);
|
|
37673
38216
|
this.deps.log(`pack run ${this.state.runId} stalled at stage ${index + 1}: ${reason}`);
|
|
37674
38217
|
return false;
|
|
37675
38218
|
}
|
|
37676
|
-
|
|
37677
|
-
|
|
38219
|
+
priorOutcomes(index) {
|
|
38220
|
+
const out2 = [];
|
|
38221
|
+
for (let i = 0; i < index; i++) {
|
|
37678
38222
|
const s = this.state.stages[i];
|
|
37679
|
-
if (s.status === "done" && 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" });
|
|
37680
38226
|
}
|
|
37681
|
-
return
|
|
38227
|
+
return out2;
|
|
37682
38228
|
}
|
|
37683
38229
|
};
|
|
37684
38230
|
|
|
@@ -37692,7 +38238,7 @@ function getActivePackRunner() {
|
|
|
37692
38238
|
}
|
|
37693
38239
|
|
|
37694
38240
|
// src/packs/handlers.ts
|
|
37695
|
-
var TERMINAL_STATUSES =
|
|
38241
|
+
var TERMINAL_STATUSES = TERMINAL_PACK_STATUSES;
|
|
37696
38242
|
var PACK_ACTIONS = /* @__PURE__ */ new Set(["pause", "resume", "retry_stage", "skip_stage", "abort"]);
|
|
37697
38243
|
function buildPackRunnerDeps(ctx) {
|
|
37698
38244
|
const cwd = ctx.opts.cwd;
|
|
@@ -37712,10 +38258,10 @@ function buildPackRunnerDeps(ctx) {
|
|
|
37712
38258
|
ctx.history.appendUserPrompt(displayLine);
|
|
37713
38259
|
await ctx.client.prompt(prompt);
|
|
37714
38260
|
const text = ctx.streaming.getCurrentText();
|
|
37715
|
-
await ctx.streaming.closeTurnWithInteractiveDetection();
|
|
38261
|
+
const awaitingUser = await ctx.streaming.closeTurnWithInteractiveDetection();
|
|
37716
38262
|
ctx.history.appendAgentReply(text);
|
|
37717
38263
|
await ctx.history.flush();
|
|
37718
|
-
return text;
|
|
38264
|
+
return { text, awaitingUser };
|
|
37719
38265
|
},
|
|
37720
38266
|
cancel: () => ctx.client.cancel(),
|
|
37721
38267
|
mountSkills: (skillIds) => {
|
|
@@ -37726,16 +38272,27 @@ function buildPackRunnerDeps(ctx) {
|
|
|
37726
38272
|
log.warn("packs", `skill mount failed for ${id}: ${err.message}`);
|
|
37727
38273
|
}
|
|
37728
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
|
+
}
|
|
37729
38284
|
}
|
|
37730
38285
|
},
|
|
37731
38286
|
gates: {
|
|
37732
38287
|
head: () => gitHead(run, cwd),
|
|
37733
38288
|
canonicalCommit: (sha) => canonicalCommit(run, cwd, sha),
|
|
37734
38289
|
diffStat: (from, to) => diffStat(run, cwd, from, to),
|
|
38290
|
+
changedFiles: (from, to) => changedFiles(run, cwd, from, to),
|
|
37735
38291
|
runChecks: async () => {
|
|
37736
38292
|
const command2 = detectChecksCommand(cwd);
|
|
37737
38293
|
return command2 ? runChecks(run, cwd, command2) : null;
|
|
37738
|
-
}
|
|
38294
|
+
},
|
|
38295
|
+
readFile: async (relPath) => readWorkspaceFile(cwd, relPath)
|
|
37739
38296
|
},
|
|
37740
38297
|
ledger: {
|
|
37741
38298
|
saveRun: (state) => saveRun(cwd, state),
|
|
@@ -37753,19 +38310,38 @@ function buildPackRunnerDeps(ctx) {
|
|
|
37753
38310
|
log: (message) => log.info("packs", message)
|
|
37754
38311
|
};
|
|
37755
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
|
+
}
|
|
37756
38328
|
var packStartH = async (ctx) => {
|
|
37757
38329
|
const payload = ctx.cmd.payload;
|
|
37758
38330
|
const packId = typeof payload?.packId === "string" ? payload.packId : "";
|
|
37759
38331
|
const task = typeof payload?.task === "string" ? payload.task.trim() : "";
|
|
37760
38332
|
if (!isPackId(packId)) {
|
|
37761
|
-
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38333
|
+
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38334
|
+
error: `unknown pack: ${packId || "(none)"}`
|
|
38335
|
+
});
|
|
37762
38336
|
return;
|
|
37763
38337
|
}
|
|
37764
38338
|
if (task.length === 0) {
|
|
37765
|
-
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38339
|
+
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38340
|
+
error: "pack_start requires a non-empty task"
|
|
38341
|
+
});
|
|
37766
38342
|
return;
|
|
37767
38343
|
}
|
|
37768
|
-
const existing =
|
|
38344
|
+
const existing = await ensureRunner(ctx);
|
|
37769
38345
|
if (existing && !TERMINAL_STATUSES.has(existing.getState().status)) {
|
|
37770
38346
|
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
37771
38347
|
error: "a pack run is already active on this session \u2014 pause/abort it first",
|
|
@@ -37788,10 +38364,12 @@ var packActionH = async (ctx) => {
|
|
|
37788
38364
|
const payload = ctx.cmd.payload;
|
|
37789
38365
|
const action = typeof payload?.action === "string" ? payload.action : "";
|
|
37790
38366
|
if (!PACK_ACTIONS.has(action)) {
|
|
37791
|
-
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38367
|
+
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
38368
|
+
error: `unknown pack action: ${action || "(none)"}`
|
|
38369
|
+
});
|
|
37792
38370
|
return;
|
|
37793
38371
|
}
|
|
37794
|
-
const runner =
|
|
38372
|
+
const runner = await ensureRunner(ctx);
|
|
37795
38373
|
if (!runner) {
|
|
37796
38374
|
await ctx.relay.sendResult(ctx.cmd.id, "failed", {
|
|
37797
38375
|
error: "no active pack run in this session",
|
|
@@ -37799,11 +38377,20 @@ var packActionH = async (ctx) => {
|
|
|
37799
38377
|
});
|
|
37800
38378
|
return;
|
|
37801
38379
|
}
|
|
37802
|
-
const
|
|
37803
|
-
|
|
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 });
|
|
37804
38390
|
};
|
|
37805
38391
|
var packStatusH = async (ctx) => {
|
|
37806
|
-
const
|
|
38392
|
+
const runner = await ensureRunner(ctx);
|
|
38393
|
+
const state = runner?.getState() ?? loadLatestRun(ctx.opts.cwd);
|
|
37807
38394
|
await ctx.relay.sendResult(ctx.cmd.id, "completed", { state });
|
|
37808
38395
|
};
|
|
37809
38396
|
|
|
@@ -40600,6 +41187,7 @@ async function runAcpSession(opts) {
|
|
|
40600
41187
|
};
|
|
40601
41188
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
40602
41189
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
41190
|
+
relay.setOnSessionGone(() => void shutdown("SIGTERM"));
|
|
40603
41191
|
process.once("SIGHUP", () => void shutdown("SIGHUP"));
|
|
40604
41192
|
await new Promise(() => {
|
|
40605
41193
|
});
|
|
@@ -42730,6 +43318,7 @@ async function runBatonSession(opts) {
|
|
|
42730
43318
|
process.once("SIGINT", onSignal);
|
|
42731
43319
|
process.once("SIGTERM", onSignal);
|
|
42732
43320
|
process.once("SIGHUP", onSignal);
|
|
43321
|
+
relay.setOnSessionGone(onSignal);
|
|
42733
43322
|
showInfo(`Starting ${opts.agent} baton (local) \u2014 native TUI + mobile take-control\u2026`);
|
|
42734
43323
|
await controller.begin();
|
|
42735
43324
|
relay.start();
|
|
@@ -43170,6 +43759,9 @@ async function start(requestedAgent, presetSession) {
|
|
|
43170
43759
|
})
|
|
43171
43760
|
);
|
|
43172
43761
|
ctx.relay = relay;
|
|
43762
|
+
relay.setOnSessionGone(() => {
|
|
43763
|
+
void sigintHandler();
|
|
43764
|
+
});
|
|
43173
43765
|
void beadsReady.then((started) => {
|
|
43174
43766
|
ctx.beads = started;
|
|
43175
43767
|
});
|
|
@@ -46422,7 +47014,7 @@ async function autoUpgradeBeforeCriticalCommand() {
|
|
|
46422
47014
|
if (process.env.NODE_ENV === "test") return;
|
|
46423
47015
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
46424
47016
|
if (process.env.CI) return;
|
|
46425
|
-
const current2 = true ? "2.75.
|
|
47017
|
+
const current2 = true ? "2.75.9" : null;
|
|
46426
47018
|
if (!current2) return;
|
|
46427
47019
|
const cache = readCache();
|
|
46428
47020
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|
|
@@ -46439,7 +47031,7 @@ function checkForUpdates() {
|
|
|
46439
47031
|
if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
|
|
46440
47032
|
if (process.env.CI) return;
|
|
46441
47033
|
if (!process.stdout.isTTY) return;
|
|
46442
|
-
const current2 = true ? "2.75.
|
|
47034
|
+
const current2 = true ? "2.75.9" : null;
|
|
46443
47035
|
if (!current2) return;
|
|
46444
47036
|
const cache = readCache();
|
|
46445
47037
|
const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
|
|
@@ -46462,7 +47054,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
|
|
|
46462
47054
|
var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
|
|
46463
47055
|
var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
|
|
46464
47056
|
function currentCliVersion() {
|
|
46465
|
-
return true ? "2.75.
|
|
47057
|
+
return true ? "2.75.9" : null;
|
|
46466
47058
|
}
|
|
46467
47059
|
async function installedVersion(deps) {
|
|
46468
47060
|
const ls = await deps.run(
|
|
@@ -48202,7 +48794,7 @@ function checkChokidar() {
|
|
|
48202
48794
|
}
|
|
48203
48795
|
async function doctor(args2 = []) {
|
|
48204
48796
|
const json = args2.includes("--json");
|
|
48205
|
-
const cliVersion = true ? "2.75.
|
|
48797
|
+
const cliVersion = true ? "2.75.9" : "0.0.0-dev";
|
|
48206
48798
|
const apiBase2 = resolveApiBaseUrl();
|
|
48207
48799
|
const diagnosticId = (0, import_node_crypto12.randomUUID)();
|
|
48208
48800
|
log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
|
|
@@ -48726,7 +49318,7 @@ async function mcpWarm(argv = [], opts = {}) {
|
|
|
48726
49318
|
// src/commands/version.ts
|
|
48727
49319
|
var import_picocolors15 = __toESM(require("picocolors"));
|
|
48728
49320
|
function version2() {
|
|
48729
|
-
const v = true ? "2.75.
|
|
49321
|
+
const v = true ? "2.75.9" : "unknown";
|
|
48730
49322
|
console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
|
|
48731
49323
|
}
|
|
48732
49324
|
|