ralphctl 0.4.0 → 0.4.1

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.
@@ -1681,6 +1681,37 @@ function collectRepoIds(tasks) {
1681
1681
 
1682
1682
  // src/business/usecases/execute.ts
1683
1683
  import { basename } from "path";
1684
+
1685
+ // src/business/usecases/recover-dirty-tree.ts
1686
+ async function recoverDirtyTree(deps, params) {
1687
+ const { external, logger, signalBus } = deps;
1688
+ const { sprintId, taskId, taskName, repoPath } = params;
1689
+ if (!external.hasUncommittedChanges(repoPath)) return;
1690
+ logger.warn(
1691
+ `Dirty tree after "${taskName}" \u2014 auto-committing on the harness's behalf. The agent should commit its own work; see prompt guidance.`,
1692
+ { taskId, projectPath: repoPath }
1693
+ );
1694
+ signalBus.emit({
1695
+ type: "signal",
1696
+ signal: {
1697
+ type: "note",
1698
+ text: `harness auto-commit: dirty tree after task "${taskName}" settlement`,
1699
+ timestamp: /* @__PURE__ */ new Date()
1700
+ },
1701
+ ctx: { sprintId, taskId, projectPath: repoPath }
1702
+ });
1703
+ const message = `chore(harness): auto-commit leftover changes from "${taskName}"`;
1704
+ try {
1705
+ await external.autoCommit(repoPath, message);
1706
+ } catch (err) {
1707
+ logger.error(`Auto-commit failed in ${repoPath}: ${err instanceof Error ? err.message : String(err)}`, {
1708
+ taskId,
1709
+ projectPath: repoPath
1710
+ });
1711
+ }
1712
+ }
1713
+
1714
+ // src/business/usecases/execute.ts
1684
1715
  var ExecuteTasksUseCase = class {
1685
1716
  constructor(persistence, aiSession, promptBuilder, parser, ui, logger, external, fs, signalParser2, signalHandler, signalBus) {
1686
1717
  this.persistence = persistence;
@@ -1940,6 +1971,10 @@ ${instructions}`;
1940
1971
  );
1941
1972
  if (finishStatus === "done") finishStatus = "failed";
1942
1973
  }
1974
+ await recoverDirtyTree(
1975
+ { external: this.external, logger: this.logger, signalBus: this.signalBus },
1976
+ { sprintId: sprint.id, taskId: syntheticTask.id, taskName: syntheticTask.name, repoPath }
1977
+ );
1943
1978
  this.signalBus.emit({
1944
1979
  type: "task-finished",
1945
1980
  sprintId: sprint.id,
@@ -2760,6 +2795,19 @@ function evaluateTask(deps) {
2760
2795
  });
2761
2796
  }
2762
2797
 
2798
+ // src/business/pipelines/execute/steps/recover-dirty-tree.ts
2799
+ function recoverDirtyTree2(deps) {
2800
+ return step("recover-dirty-tree", async (ctx) => {
2801
+ const { task, sprint } = ctx;
2802
+ const repoPath = await deps.persistence.resolveRepoPath(task.repoId);
2803
+ await recoverDirtyTree(
2804
+ { external: deps.external, logger: deps.logger, signalBus: deps.signalBus },
2805
+ { sprintId: sprint.id, taskId: task.id, taskName: task.name, repoPath }
2806
+ );
2807
+ return Result.ok({});
2808
+ });
2809
+ }
2810
+
2763
2811
  // src/business/pipelines/execute/steps/mark-done.ts
2764
2812
  function markDone(deps) {
2765
2813
  return step("mark-done", async (ctx) => {
@@ -2823,6 +2871,14 @@ function createPerTaskPipeline(deps, useCase, options = {}) {
2823
2871
  options
2824
2872
  })
2825
2873
  ),
2874
+ trace(
2875
+ recoverDirtyTree2({
2876
+ persistence: deps.persistence,
2877
+ external: deps.external,
2878
+ logger: deps.logger,
2879
+ signalBus: deps.signalBus
2880
+ })
2881
+ ),
2826
2882
  trace(markDone({ persistence: deps.persistence, logger: deps.logger, signalBus: deps.signalBus }))
2827
2883
  ]);
2828
2884
  }
@@ -5217,6 +5273,25 @@ function hasUncommittedChanges(cwd) {
5217
5273
  }
5218
5274
  return result.stdout.trim().length > 0;
5219
5275
  }
5276
+ function autoCommit(cwd, message) {
5277
+ assertSafeCwd(cwd);
5278
+ const add = spawnSync3("git", ["add", "-A"], {
5279
+ cwd,
5280
+ encoding: "utf-8",
5281
+ stdio: ["pipe", "pipe", "pipe"]
5282
+ });
5283
+ if (add.status !== 0) {
5284
+ throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
5285
+ }
5286
+ const commit = spawnSync3("git", ["commit", "-m", message], {
5287
+ cwd,
5288
+ encoding: "utf-8",
5289
+ stdio: ["pipe", "pipe", "pipe"]
5290
+ });
5291
+ if (commit.status !== 0) {
5292
+ throw new Error(`Failed to commit in ${cwd}: ${commit.stderr.trim() || commit.stdout.trim()}`);
5293
+ }
5294
+ }
5220
5295
  function generateBranchName(sprintId) {
5221
5296
  return `ralphctl/${sprintId}`;
5222
5297
  }
@@ -5276,6 +5351,10 @@ var DefaultExternalAdapter = class {
5276
5351
  hasUncommittedChanges(projectPath) {
5277
5352
  return hasUncommittedChanges(projectPath);
5278
5353
  }
5354
+ autoCommit(projectPath, message) {
5355
+ autoCommit(projectPath, message);
5356
+ return Promise.resolve();
5357
+ }
5279
5358
  createAndCheckoutBranch(projectPath, branchName) {
5280
5359
  createAndCheckoutBranch(projectPath, branchName);
5281
5360
  }
@@ -52,7 +52,7 @@ import {
52
52
  updateTask,
53
53
  updateTaskStatus,
54
54
  validateImportTasks
55
- } from "./chunk-JYCGQA2D.mjs";
55
+ } from "./chunk-3HJNVQ7N.mjs";
56
56
  import {
57
57
  fetchIssueFromUrl,
58
58
  formatIssueContext,
@@ -177,7 +177,7 @@ import {
177
177
  // package.json
178
178
  var package_default = {
179
179
  name: "ralphctl",
180
- version: "0.4.0",
180
+ version: "0.4.1",
181
181
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
182
182
  homepage: "https://github.com/lukas-grigis/ralphctl",
183
183
  type: "module",
package/dist/cli.mjs CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  ticketRefineCommand,
42
42
  ticketRemoveCommand,
43
43
  ticketShowCommand
44
- } from "./chunk-MRN3Z2XC.mjs";
44
+ } from "./chunk-SM4GGZSU.mjs";
45
45
  import {
46
46
  projectAddCommand
47
47
  } from "./chunk-D2YGPLIV.mjs";
@@ -55,7 +55,7 @@ import "./chunk-NUYQK5MN.mjs";
55
55
  import {
56
56
  getTasks,
57
57
  sprintStartCommand
58
- } from "./chunk-JYCGQA2D.mjs";
58
+ } from "./chunk-3HJNVQ7N.mjs";
59
59
  import {
60
60
  truncate
61
61
  } from "./chunk-JOQO4HMM.mjs";
@@ -705,7 +705,7 @@ async function main() {
705
705
  const isBare = argv.length <= 2;
706
706
  const isInteractive = argv[2] === "interactive";
707
707
  if (isBare || isInteractive) {
708
- const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
708
+ const { mountInkApp } = await import("./mount-2N6H5CWA.mjs");
709
709
  const { fallback } = await mountInkApp({ initialView: "repl" });
710
710
  if (!fallback) return;
711
711
  printBanner();
@@ -716,10 +716,10 @@ async function main() {
716
716
  return;
717
717
  }
718
718
  if (argv[2] === "sprint" && argv[3] === "start") {
719
- const { parseSprintStartArgs } = await import("./start-D35SOXMM.mjs");
719
+ const { parseSprintStartArgs } = await import("./start-IUDCXIEA.mjs");
720
720
  const parsed = parseSprintStartArgs(argv.slice(4));
721
721
  if (parsed.ok) {
722
- const { mountInkApp } = await import("./mount-XMN3S4W6.mjs");
722
+ const { mountInkApp } = await import("./mount-2N6H5CWA.mjs");
723
723
  const { getSharedDeps } = await import("./bootstrap-FMHG6DRY.mjs");
724
724
  let sprintId;
725
725
  try {
@@ -62,7 +62,7 @@ import {
62
62
  ticketShowCommand,
63
63
  useCurrentPrompt,
64
64
  validateConfigValue
65
- } from "./chunk-MRN3Z2XC.mjs";
65
+ } from "./chunk-SM4GGZSU.mjs";
66
66
  import {
67
67
  PromptCancelledError,
68
68
  projectAddCommand
@@ -109,7 +109,7 @@ import {
109
109
  sprintStartCommand,
110
110
  updateTaskStatus,
111
111
  withSuspendedTui
112
- } from "./chunk-JYCGQA2D.mjs";
112
+ } from "./chunk-3HJNVQ7N.mjs";
113
113
  import {
114
114
  addTicket,
115
115
  allRequirementsApproved,
@@ -31,7 +31,10 @@ something entirely new (create a file, add a feature, tweak a script), do exactl
31
31
  it passes. If no check script is configured, skip this step.
32
32
  4. **Output verification results** — Wrap any verification output in `<task-verified>...</task-verified>`. If you
33
33
  skipped step 3, emit `<task-verified>no check script configured; change applied</task-verified>`.
34
- 5. **Signal completion** — Output `<task-complete>` once the change is applied and verification (if any) passed.
34
+ 5. **Commit your work** — Stage the modified files and create a git commit with a descriptive message summarising the
35
+ feedback you implemented. The harness refuses to mark the task done with a dirty working tree.
36
+ 6. **Signal completion** — Output `<task-complete>` once the change is applied, verification (if any) passed, and the
37
+ commit has landed.
35
38
 
36
39
  Only signal `<task-blocked>reason</task-blocked>` if the feedback is literally impossible to carry out (e.g., asks
37
40
  you to edit a file in a repository you don't have access to). Ambiguity is **not** a blocker — make a reasonable
@@ -42,6 +45,8 @@ interpretation and proceed.
42
45
  - **The feedback is the authoritative instruction** — implement it even if it seems unrelated to the completed tasks.
43
46
  - **Do the smallest change that fully satisfies the feedback** — no speculative refactors, no adjacent cleanup.
44
47
  - **Make the edits — don't just describe them** — the harness does not apply edits for you; you must write the files.
48
+ - **Must commit** — Create a git commit before signaling completion. Uncommitted changes leave the sprint branch dirty
49
+ and block sprint close.
45
50
 
46
51
  </constraints>
47
52
 
@@ -21,6 +21,11 @@ These verification criteria are the pre-agreed definition of "done" — your pri
21
21
 
22
22
  ## Review Protocol
23
23
 
24
+ **You are a reviewer — do not edit files.** If you believe a fix is needed, emit `<evaluation-failed>` with a concrete
25
+ critique; the harness will resume the generator to apply the fix. Do not run `git stash`, do not edit tests, do not
26
+ create commits. Your tools are read-only: `git status`, `git log`, `git diff`, file reads, and running existing check
27
+ scripts. Any write operation is a protocol violation.
28
+
24
29
  You are working in this project directory:
25
30
 
26
31
  ```
@@ -37,7 +42,8 @@ Run deterministic checks first — these are cheap, fast, and authoritative.
37
42
 
38
43
  1. **Run the check script** (if provided above) — this is the same gate the harness uses post-task. If it fails, the
39
44
  implementation fails regardless of how good the code looks. Record the output.
40
- 2. **Run `git status`** — uncommitted changes may indicate incomplete work
45
+ 2. **Run `git status`** — the tree MUST be clean. Uncommitted changes from the generator are a Completeness failure;
46
+ uncommitted changes from you are a protocol violation.
41
47
  3. **Run `git log --oneline -10`** — identify which commits belong to this task
42
48
 
43
49
  Computational results are ground truth. If the check script fails, stop early — the implementation does not pass.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  parseSprintStartArgs,
4
4
  sprintStartCommand
5
- } from "./chunk-JYCGQA2D.mjs";
5
+ } from "./chunk-3HJNVQ7N.mjs";
6
6
  import "./chunk-JOQO4HMM.mjs";
7
7
  import "./chunk-CFUVE2BP.mjs";
8
8
  import "./chunk-747KW2RW.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphctl",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code & GitHub Copilot across repositories",
5
5
  "homepage": "https://github.com/lukas-grigis/ralphctl",
6
6
  "type": "module",