@xdevops/issue-auto-finish 1.0.96 → 1.0.98

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 (43) hide show
  1. package/dist/{PtyRunner-UD3M7GNM.js → PtyRunner-AVP7C4HC.js} +2 -2
  2. package/dist/ai-runner/AIRunner.d.ts +2 -4
  3. package/dist/ai-runner/AIRunner.d.ts.map +1 -1
  4. package/dist/ai-runner/PlanFileResolver.d.ts +20 -3
  5. package/dist/ai-runner/PlanFileResolver.d.ts.map +1 -1
  6. package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
  7. package/dist/{ai-runner-6OQYGU56.js → ai-runner-GPHHQKUT.js} +2 -2
  8. package/dist/{analyze-LZHS3MHW.js → analyze-SYJXCCU7.js} +2 -2
  9. package/dist/{braindump-7QDUYUZW.js → braindump-QIUTH777.js} +2 -2
  10. package/dist/{chunk-P6KYWBMT.js → chunk-APROB5LF.js} +9 -9
  11. package/dist/{chunk-75ANI33A.js → chunk-CKYGI2V2.js} +1 -1
  12. package/dist/{chunk-W553OKAJ.js → chunk-DUQUGPMI.js} +32 -66
  13. package/dist/chunk-DUQUGPMI.js.map +1 -0
  14. package/dist/{chunk-AFGUFM6H.js → chunk-URL4HZ66.js} +2 -2
  15. package/dist/{chunk-SJSVO46Z.js → chunk-XOOKCEAK.js} +92 -76
  16. package/dist/chunk-XOOKCEAK.js.map +1 -0
  17. package/dist/cli.js +5 -5
  18. package/dist/hooks/HookInjector.d.ts +0 -6
  19. package/dist/hooks/HookInjector.d.ts.map +1 -1
  20. package/dist/index.js +4 -4
  21. package/dist/{init-Q4DTX6JN.js → init-FDXIJNXF.js} +2 -2
  22. package/dist/lib.js +2 -2
  23. package/dist/orchestrator/steps/PhaseHelpers.d.ts +1 -1
  24. package/dist/orchestrator/steps/PhaseHelpers.d.ts.map +1 -1
  25. package/dist/orchestrator/steps/SetupStep.d.ts +0 -7
  26. package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
  27. package/dist/phases/BasePhase.d.ts.map +1 -1
  28. package/dist/{restart-CEFVMQLL.js → restart-BRO6NIID.js} +2 -2
  29. package/dist/run.js +4 -4
  30. package/dist/{start-JYVFJZCL.js → start-ZIJDXV56.js} +2 -2
  31. package/package.json +1 -1
  32. package/dist/chunk-SJSVO46Z.js.map +0 -1
  33. package/dist/chunk-W553OKAJ.js.map +0 -1
  34. /package/dist/{PtyRunner-UD3M7GNM.js.map → PtyRunner-AVP7C4HC.js.map} +0 -0
  35. /package/dist/{ai-runner-6OQYGU56.js.map → ai-runner-GPHHQKUT.js.map} +0 -0
  36. /package/dist/{analyze-LZHS3MHW.js.map → analyze-SYJXCCU7.js.map} +0 -0
  37. /package/dist/{braindump-7QDUYUZW.js.map → braindump-QIUTH777.js.map} +0 -0
  38. /package/dist/{chunk-P6KYWBMT.js.map → chunk-APROB5LF.js.map} +0 -0
  39. /package/dist/{chunk-75ANI33A.js.map → chunk-CKYGI2V2.js.map} +0 -0
  40. /package/dist/{chunk-AFGUFM6H.js.map → chunk-URL4HZ66.js.map} +0 -0
  41. /package/dist/{init-Q4DTX6JN.js.map → init-FDXIJNXF.js.map} +0 -0
  42. /package/dist/{restart-CEFVMQLL.js.map → restart-BRO6NIID.js.map} +0 -0
  43. /package/dist/{start-JYVFJZCL.js.map → start-ZIJDXV56.js.map} +0 -0
@@ -469,7 +469,7 @@ function createSetupRouter(deps = {}) {
469
469
  sse.write({ step: "analyzing", message: "Running AI analysis..." });
470
470
  try {
471
471
  const { loadConfig } = await import("./config-23TBYFP5.js");
472
- const { createAIRunner } = await import("./ai-runner-6OQYGU56.js");
472
+ const { createAIRunner } = await import("./ai-runner-GPHHQKUT.js");
473
473
  const config = loadConfig();
474
474
  const runner = createAIRunner(config.ai);
475
475
  const knowledge = await analyze({ workDir, aiRunner: runner });
@@ -549,4 +549,4 @@ function createSetupRouter(deps = {}) {
549
549
  export {
550
550
  createSetupRouter
551
551
  };
552
- //# sourceMappingURL=chunk-AFGUFM6H.js.map
552
+ //# sourceMappingURL=chunk-URL4HZ66.js.map
@@ -24,10 +24,11 @@ var PLAN_DIRS = {
24
24
  "claude-internal": path.join(os.homedir(), ".claude-internal", "plans"),
25
25
  "codebuddy": path.join(os.homedir(), ".codebuddy", "plans")
26
26
  };
27
- var PLAN_METADATA_PATTERNS = [
28
- /^00-plan-status\.md$/
29
- ];
30
27
  var PlanFileResolver = class _PlanFileResolver {
28
+ /** Plan files older than this are removed before snapshot (2 hours). */
29
+ static STALE_THRESHOLD_MS = 2 * 60 * 60 * 1e3;
30
+ /** Resolved plan files shorter than this are considered empty/fragment. */
31
+ static MIN_PLAN_BYTES = 200;
31
32
  plansDir;
32
33
  beforeFiles = /* @__PURE__ */ new Map();
33
34
  constructor(plansDir) {
@@ -38,14 +39,49 @@ var PlanFileResolver = class _PlanFileResolver {
38
39
  const dir = PLAN_DIRS[agentMode];
39
40
  return new _PlanFileResolver(dir);
40
41
  }
41
- /** Take a snapshot of existing plan files before the plan phase starts. */
42
- takeBeforeSnapshot() {
42
+ /**
43
+ * Take a snapshot of existing plan files before the plan phase starts.
44
+ * Optionally removes stale files (older than STALE_THRESHOLD_MS) to reduce
45
+ * noise in the snapshot diff.
46
+ */
47
+ takeBeforeSnapshot(cleanStale = true) {
48
+ if (cleanStale) {
49
+ this.removeStaleFiles();
50
+ }
43
51
  this.beforeFiles = this.listFiles();
44
52
  logger2.info("Plan file snapshot taken", {
45
53
  dir: this.plansDir,
46
54
  fileCount: this.beforeFiles.size
47
55
  });
48
56
  }
57
+ /**
58
+ * Remove plan files older than STALE_THRESHOLD_MS to prevent accumulation
59
+ * that confuses snapshot-based resolution on long-lived machines.
60
+ */
61
+ removeStaleFiles() {
62
+ if (!fs.existsSync(this.plansDir)) return;
63
+ const now = Date.now();
64
+ let removed = 0;
65
+ try {
66
+ for (const entry of fs.readdirSync(this.plansDir)) {
67
+ if (!entry.endsWith(".md")) continue;
68
+ const fullPath = path.join(this.plansDir, entry);
69
+ try {
70
+ const stat = fs.statSync(fullPath);
71
+ if (stat.isFile() && now - stat.mtimeMs > _PlanFileResolver.STALE_THRESHOLD_MS) {
72
+ fs.unlinkSync(fullPath);
73
+ removed++;
74
+ }
75
+ } catch {
76
+ }
77
+ }
78
+ } catch (err) {
79
+ logger2.warn("Failed to clean stale plan files", { dir: this.plansDir, err });
80
+ }
81
+ if (removed > 0) {
82
+ logger2.info("Removed stale plan files", { dir: this.plansDir, removed });
83
+ }
84
+ }
49
85
  /**
50
86
  * Check if any new or modified plan files exist since the before-snapshot.
51
87
  * Used as an artifact gate by detectCompletion to prevent premature
@@ -85,19 +121,19 @@ var PlanFileResolver = class _PlanFileResolver {
85
121
  });
86
122
  return this.fallbackByMtime(afterFiles, contentHint);
87
123
  }
88
- candidates.sort((a, b) => b.mtime - a.mtime);
89
124
  if (candidates.length === 1) {
90
- return this.readCandidate(candidates[0].path, candidates[0].mtime);
125
+ return this.readAndValidate(candidates[0].path, candidates[0].mtime);
91
126
  }
92
127
  if (contentHint) {
93
128
  const matched = this.matchByContent(candidates, contentHint);
94
129
  if (matched) return matched;
95
130
  }
131
+ candidates.sort((a, b) => b.mtime - a.mtime);
96
132
  logger2.info("Multiple new plan files found, using most recent", {
97
133
  count: candidates.length,
98
134
  selected: path.basename(candidates[0].path)
99
135
  });
100
- return this.readCandidate(candidates[0].path, candidates[0].mtime);
136
+ return this.readAndValidate(candidates[0].path, candidates[0].mtime);
101
137
  }
102
138
  /**
103
139
  * Build a content hint string from issue metadata for content-based matching.
@@ -131,16 +167,12 @@ var PlanFileResolver = class _PlanFileResolver {
131
167
  findNewFiles(afterFiles) {
132
168
  const candidates = [];
133
169
  for (const [filePath, mtime] of afterFiles) {
134
- if (!this.beforeFiles.has(filePath) && !this.isMetadataFile(filePath)) {
170
+ if (!this.beforeFiles.has(filePath)) {
135
171
  candidates.push({ path: filePath, mtime });
136
172
  }
137
173
  }
138
174
  return candidates;
139
175
  }
140
- isMetadataFile(filePath) {
141
- const basename = path.basename(filePath);
142
- return PLAN_METADATA_PATTERNS.some((pattern) => pattern.test(basename));
143
- }
144
176
  /**
145
177
  * Fallback: if no new files found (rare case — plan might have overwritten
146
178
  * an existing file), find the most recently modified file.
@@ -168,35 +200,44 @@ var PlanFileResolver = class _PlanFileResolver {
168
200
  file: path.basename(newestPath),
169
201
  ageMs: Date.now() - newestMtime
170
202
  });
171
- return this.readCandidate(newestPath, newestMtime);
203
+ return this.readAndValidate(newestPath, newestMtime);
172
204
  }
173
205
  matchByContent(candidates, contentHint) {
174
- const matches = [];
175
206
  for (const candidate of candidates) {
176
207
  try {
177
208
  const content = fs.readFileSync(candidate.path, "utf-8");
178
209
  if (this.contentMatches(content, contentHint)) {
179
- matches.push({ sourcePath: candidate.path, content });
210
+ logger2.info("Plan file matched by content", {
211
+ file: path.basename(candidate.path),
212
+ hint: contentHint.slice(0, 50)
213
+ });
214
+ return { sourcePath: candidate.path, content };
180
215
  }
181
216
  } catch {
182
217
  continue;
183
218
  }
184
219
  }
185
- if (matches.length === 0) return null;
186
- matches.sort((a, b) => b.content.length - a.content.length);
187
- const best = matches[0];
188
- logger2.info("Plan file matched by content", {
189
- file: path.basename(best.sourcePath),
190
- hint: contentHint.slice(0, 50),
191
- matchCount: matches.length,
192
- size: best.content.length
193
- });
194
- return best;
220
+ return null;
195
221
  }
196
222
  contentMatches(content, hint) {
197
223
  const parts = hint.split("|");
198
224
  return parts.some((part) => content.includes(part));
199
225
  }
226
+ /**
227
+ * Read and validate a candidate, warning if content is below MIN_PLAN_BYTES.
228
+ * Returns the file even if short (caller may still use it as best-effort).
229
+ */
230
+ readAndValidate(filePath, mtime) {
231
+ const result = this.readCandidate(filePath, mtime);
232
+ if (result && result.content.length < _PlanFileResolver.MIN_PLAN_BYTES) {
233
+ logger2.warn("Resolved plan file is suspiciously short (possible fragment)", {
234
+ file: path.basename(filePath),
235
+ size: result.content.length,
236
+ threshold: _PlanFileResolver.MIN_PLAN_BYTES
237
+ });
238
+ }
239
+ return result;
240
+ }
200
241
  readCandidate(filePath, mtime) {
201
242
  try {
202
243
  const content = fs.readFileSync(filePath, "utf-8");
@@ -510,7 +551,7 @@ var HookInjector = class {
510
551
  { name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
511
552
  { name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
512
553
  { name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
513
- { name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile, ctx.planDir, ctx.phaseExpectedArtifacts?.[0] ?? ctx.expectedArtifacts[0]) },
554
+ { name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile) },
514
555
  { name: "permission.sh", content: buildPermissionScript(eventsFile) },
515
556
  { name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
516
557
  { name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) },
@@ -742,9 +783,8 @@ if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
742
783
  "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
743
784
  fi
744
785
 
745
- BYTES_EVT=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
746
- printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
747
- "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES_EVT" >> ${quote(eventsFile)}
786
+ printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
787
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
748
788
  exit 0
749
789
  `;
750
790
  }
@@ -766,23 +806,11 @@ fi
766
806
  exit 0
767
807
  `;
768
808
  }
769
- function buildExitPlanModeScript(eventsFile, planDir, planArtifact) {
809
+ function buildExitPlanModeScript(eventsFile) {
770
810
  return `#!/bin/bash
771
811
  set -euo pipefail
772
812
  INPUT=$(cat)
773
813
 
774
- PLAN_DIR=${quote(planDir)}
775
- PLAN_ARTIFACT=${quote(planArtifact ?? "01-plan.md")}
776
-
777
- PLAN_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.plan // empty')
778
- if [ -n "$PLAN_CONTENT" ]; then
779
- mkdir -p "$PLAN_DIR"
780
- printf '%s' "$PLAN_CONTENT" > "$PLAN_DIR/$PLAN_ARTIFACT"
781
- BYTES=$(wc -c < "$PLAN_DIR/$PLAN_ARTIFACT")
782
- printf '{"ts":"%s","event":"plan_captured","file":"%s","path":"%s/%s","bytes":%s}\\n' \\
783
- "$(date -u +%FT%TZ)" "$PLAN_ARTIFACT" "$PLAN_DIR" "$PLAN_ARTIFACT" "$BYTES" >> ${quote(eventsFile)}
784
- fi
785
-
786
814
  printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
787
815
 
788
816
  echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
@@ -1455,7 +1483,7 @@ var PtyRunner = class {
1455
1483
  const resolver = PlanFileResolver.forRunner(agentMode);
1456
1484
  resolver.takeBeforeSnapshot();
1457
1485
  const issueIid = extractIidFromPath(workDir);
1458
- const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid) : void 0;
1486
+ const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid, options.issueTitle) : void 0;
1459
1487
  if (continueSession) {
1460
1488
  logger6.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
1461
1489
  } else {
@@ -1476,8 +1504,7 @@ var PtyRunner = class {
1476
1504
  const planResult = await this.detectCompletion(sessionId, {
1477
1505
  ...options,
1478
1506
  completionSignal: PLAN_CONFIRM_RE,
1479
- artifactCheck: planArtifactCheck,
1480
- completionHookEvent: "exit_plan_mode"
1507
+ artifactCheck: planArtifactCheck
1481
1508
  }, options.onStreamEvent, continueSession);
1482
1509
  if (planResult.timedOut) {
1483
1510
  logger6.warn("Native plan mode: plan phase timed out", {
@@ -1487,21 +1514,27 @@ var PtyRunner = class {
1487
1514
  return this.buildRunResult(planResult, sessionId);
1488
1515
  }
1489
1516
  const artifactPaths = options.artifactPaths ?? [];
1490
- const MIN_PLAN_BYTES = 50;
1491
- const hookHandled = artifactPaths.length > 0 && artifactPaths.every((p) => {
1517
+ const artifactAlreadyReady = artifactPaths.length > 0 && artifactPaths.every((p) => {
1492
1518
  try {
1493
- return fs4.existsSync(p) && fs4.statSync(p).size >= MIN_PLAN_BYTES;
1519
+ return fs4.existsSync(p) && fs4.statSync(p).size >= 200;
1494
1520
  } catch {
1495
1521
  return false;
1496
1522
  }
1497
1523
  });
1498
- if (hookHandled) {
1499
- logger6.info("Native plan mode: plan captured by ExitPlanMode hook", {
1524
+ if (artifactAlreadyReady) {
1525
+ logger6.info("Native plan mode: artifact already written by agent, skipping resolver copy", {
1500
1526
  sessionId,
1501
- artifactPaths
1527
+ artifacts: artifactPaths.map((p) => path3.basename(p)),
1528
+ sizes: artifactPaths.map((p) => {
1529
+ try {
1530
+ return fs4.statSync(p).size;
1531
+ } catch {
1532
+ return 0;
1533
+ }
1534
+ })
1502
1535
  });
1503
1536
  } else {
1504
- logger6.info("Native plan mode: resolving plan file from CLI storage (fallback)", { sessionId });
1537
+ logger6.info("Native plan mode: resolving plan file from CLI storage", { sessionId });
1505
1538
  const resolved = resolver.resolve(contentHint);
1506
1539
  if (!resolved) {
1507
1540
  logger6.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
@@ -1532,12 +1565,12 @@ var PtyRunner = class {
1532
1565
  resolvedFile: resolved.sourcePath
1533
1566
  });
1534
1567
  }
1568
+ logger6.info("Native plan mode completed (deterministic copy)", {
1569
+ sessionId,
1570
+ planSource: path3.basename(resolved.sourcePath),
1571
+ artifactsCopied: artifactPaths.length
1572
+ });
1535
1573
  }
1536
- logger6.info("Native plan mode completed", {
1537
- sessionId,
1538
- hookHandled,
1539
- artifactsCopied: artifactPaths.length
1540
- });
1541
1574
  return this.buildRunResult(planResult, sessionId);
1542
1575
  }
1543
1576
  /** Map detectCompletion result to RunResult. */
@@ -1656,7 +1689,6 @@ var PtyRunner = class {
1656
1689
  const dialogClassifier = new DialogClassifier();
1657
1690
  let hookWatcher;
1658
1691
  let hookSub;
1659
- let completionHookGraceTimer;
1660
1692
  const hookInjector = new HookInjector();
1661
1693
  const eventsFilePath = hookInjector.getEventsFilePath(options.workDir);
1662
1694
  if (fs4.existsSync(eventsFilePath)) {
@@ -1697,21 +1729,6 @@ var PtyRunner = class {
1697
1729
  });
1698
1730
  }
1699
1731
  }
1700
- if (options.completionHookEvent && event.event === options.completionHookEvent && hasSubstantiveOutput && !resolved && !completionHookGraceTimer) {
1701
- logger6.info("Completion hook event detected, starting grace period", {
1702
- sessionId,
1703
- event: event.event
1704
- });
1705
- completionHookGraceTimer = setTimeout(() => {
1706
- if (!resolved) {
1707
- logger6.info("Completion hook event grace period elapsed, finishing", {
1708
- sessionId,
1709
- event: event.event
1710
- });
1711
- finish({ output: outputLines.join(""), timedOut: false });
1712
- }
1713
- }, 5e3);
1714
- }
1715
1732
  });
1716
1733
  hookWatcher.start();
1717
1734
  }
@@ -1993,7 +2010,6 @@ var PtyRunner = class {
1993
2010
  clearInterval(idleCheck);
1994
2011
  if (debounceTimer) clearTimeout(debounceTimer);
1995
2012
  if (dialogQuiesceTimer) clearTimeout(dialogQuiesceTimer);
1996
- if (completionHookGraceTimer) clearTimeout(completionHookGraceTimer);
1997
2013
  subscription.dispose();
1998
2014
  hookSub?.dispose();
1999
2015
  hookWatcher?.stop();
@@ -2020,4 +2036,4 @@ export {
2020
2036
  InputWaitController,
2021
2037
  PtyRunner
2022
2038
  };
2023
- //# sourceMappingURL=chunk-SJSVO46Z.js.map
2039
+ //# sourceMappingURL=chunk-XOOKCEAK.js.map