@markjaquith/agency 3.9.1 → 3.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -616,21 +616,20 @@ handoffs restore the terminal before starting the interactive worker, then reope
616
616
  the TUI when the worker returns. A configured
617
617
  external chooser continues to offer goals and **Browse items** through that chooser.
618
618
 
619
- | Goal | Choose in `act` | Discovery/action ID |
620
- | ------------------------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
621
- | Manage repositories | Add, link, set up, materialize, fetch, verify, rename, update, unlink, or remove aliases | `repo-add`, `repo-link`, `repo-setup`, `repo-materialize`, `repo-fetch`, `repo-verify`, `repo-rename`, `repo-remote`, `repo-unlink`, `repo-remove` |
622
- | Create work | Create a standard task, multi-phase task, investigation, epic, or a task inside an epic | `task-create`, `multi-phase-create`, `investigation-create`, `epic-create`, `task-create-in-epic` |
623
- | Split work | Add a phase / split this task; name the existing work's first phase | `split` |
624
- | Work on a task or phase | Work on this item | `work` |
625
- | Move from investigation to execution | Create implementation follow-up | `handoff` |
626
- | Review someone else's work | Review a PR, or a remote branch/commit | `review`, `review-ref` |
627
- | Close finished work | Close or reopen work → complete without a PR, drop abandoned work, refresh a merged PR, or reopen | `complete`, `drop`, `sync`, `reopen` |
628
- | Publish or update PR status | Push without a PR, refresh provider state, create a PR, mark a GitHub PR ready, or close it | `push`, `sync`, `pr`, `pr-ready`, `pr-close` |
629
- | Maintain review work | Fetch and repin a review task to its current source | `review-refresh` |
630
- | Organize work | Rename an item, move a task into/out of an epic, or add/remove a sibling dependency | `rename`, `move-to-epic`, `remove-from-epic`, `dependency-add`, `dependency-remove` |
631
- | Archive terminal work | Archive | `archive` |
632
- | See work status | See current or ready work | `current-work`, `ready-work` |
633
- | Check or refresh the workbase | Validate, diagnose, reconcile all state, or inspect/update managed integration files | `validate`, `doctor`, `sync-all`, `integration-status`, `integration-sync` |
619
+ | Goal | Choose in `act` | Discovery/action ID |
620
+ | ------------------------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
621
+ | Manage repositories | Add, link, set up, materialize, fetch, verify, rename, update, unlink, or remove aliases | `repo-add`, `repo-link`, `repo-setup`, `repo-materialize`, `repo-fetch`, `repo-verify`, `repo-rename`, `repo-remote`, `repo-unlink`, `repo-remove` |
622
+ | Create work | Create a standard task, multi-phase task, investigation, epic, or a task inside an epic | `task-create`, `multi-phase-create`, `investigation-create`, `epic-create`, `task-create-in-epic` |
623
+ | Split work | Add a phase / split this task; name the existing work's first phase | `split` |
624
+ | Work on a task or phase | Work on this item | `work` |
625
+ | Move from investigation to execution | Create implementation follow-up | `handoff` |
626
+ | Review someone else's work | Review a PR, or a remote branch/commit | `review`, `review-ref` |
627
+ | Publish or update PR status | Push without a PR, refresh provider state, create a PR, mark a GitHub PR ready, or close it | `push`, `sync`, `pr`, `pr-ready`, `pr-close` |
628
+ | Maintain review work | Fetch and repin a review task to its current source | `review-refresh` |
629
+ | Organize work | Rename an item, move a task into/out of an epic, or add/remove a sibling dependency | `rename`, `move-to-epic`, `remove-from-epic`, `dependency-add`, `dependency-remove` |
630
+ | Archive terminal work | Archive | `archive` |
631
+ | See work status | See current or ready work | `current-work`, `ready-work` |
632
+ | Check or refresh the workbase | Validate, diagnose, reconcile all state, or inspect/update managed integration files | `validate`, `doctor`, `sync-all`, `integration-status`, `integration-sync` |
634
633
 
635
634
  After creating a task, phase, review, or implementation follow-up, choose **Work
636
635
  on the new item now** or **Finish**. Only choosing Work prepares checkouts and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -292,6 +292,9 @@ const actionPresentation: Record<
292
292
  },
293
293
  }
294
294
 
295
+ export const isActActionId = (id: string): boolean =>
296
+ Object.hasOwn(actionPresentation, id)
297
+
295
298
  const present = (details: Details) => {
296
299
  const presentation = actionPresentation[details.id]
297
300
  if (!presentation)
@@ -386,12 +389,6 @@ export const actionGroups = [
386
389
  icon: "",
387
390
  actions: ["push", "sync", "pr", "pr-ready", "pr-close"],
388
391
  },
389
- {
390
- id: "close",
391
- label: "Close or reopen work",
392
- icon: "󰄬",
393
- actions: ["complete", "drop", "sync", "reopen"],
394
- },
395
392
  {
396
393
  id: "archive",
397
394
  label: "Archive finished work",
@@ -14,6 +14,7 @@ import {
14
14
  actionGroups,
15
15
  actActions,
16
16
  actionOutput,
17
+ isActActionId,
17
18
  type ActAction,
18
19
  type ActEntity,
19
20
  } from "./act-actions"
@@ -360,10 +361,7 @@ const actStep = (
360
361
  if (session && options.directory && isPath && !selectedKey && !actionId)
361
362
  session.activateTab("workbase")
362
363
  state.view = !selectedKey && !actionId ? "home" : "flow"
363
- if (
364
- actionId &&
365
- !actionGroups.some((group) => group.actions.some((id) => id === actionId))
366
- )
364
+ if (actionId && !isActActionId(actionId))
367
365
  return yield* Effect.fail(
368
366
  new Error(
369
367
  `Unknown action '${actionId}'; use agency act --json to discover actions`,
@@ -5,6 +5,7 @@ import { PhaseService } from "./PhaseService"
5
5
  import { TaskService } from "./TaskService"
6
6
  import { WorkbaseService } from "./WorkbaseService"
7
7
  import { parseGitCommits, type PushCommitMetadata } from "./push-validation"
8
+ import { isDirtyGitStatus } from "./VersionControlService"
8
9
 
9
10
  type PushCategory =
10
11
  | "precondition"
@@ -374,11 +375,11 @@ const publishGit = (
374
375
  const status = yield* git(
375
376
  fs,
376
377
  checkout,
377
- ["status", "--porcelain=v1"],
378
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
378
379
  "Failed to inspect Git status",
379
380
  "inspect",
380
381
  )
381
- if (status.stdout.length > 0) {
382
+ if (isDirtyGitStatus(status.stdout)) {
382
383
  return yield* pushError(
383
384
  "Cannot publish a dirty Git worktree; commit or discard changes first",
384
385
  "precondition",
@@ -58,6 +58,17 @@ export interface VersionControlBackend {
58
58
  ) => Effect.Effect<void, unknown, any>
59
59
  }
60
60
 
61
+ const ignoredUntrackedGitPath = /^(?:.*\/)?\.DS_Store$/
62
+
63
+ export const isDirtyGitStatus = (output: string): boolean =>
64
+ (output.includes("\0") ? output.split("\0") : output.split(/\r?\n/)).some(
65
+ (line) => {
66
+ if (!line) return false
67
+ if (!line.startsWith("?? ")) return true
68
+ return !ignoredUntrackedGitPath.test(line.slice(3))
69
+ },
70
+ )
71
+
61
72
  class VersionControlError extends Data.TaggedError("VersionControlError")<{
62
73
  readonly message: string
63
74
  }> {}
@@ -267,10 +278,20 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
267
278
  Effect.gen(function* () {
268
279
  const fs = yield* FileSystemService
269
280
  const result = yield* fs.runCommand(
270
- ["git", "-C", workspacePath, "status", "--porcelain"],
281
+ [
282
+ "git",
283
+ "-C",
284
+ workspacePath,
285
+ "status",
286
+ "--porcelain=v1",
287
+ "-z",
288
+ "--untracked-files=all",
289
+ ],
271
290
  { captureOutput: true },
272
291
  )
273
- return result.exitCode === 0 ? result.stdout.length > 0 : null
292
+ return result.exitCode === 0
293
+ ? isDirtyGitStatus(result.stdout)
294
+ : null
274
295
  }),
275
296
  fetch: (repositoryPath, remote = "origin", branch) =>
276
297
  Effect.gen(function* () {
@@ -18,7 +18,10 @@ import type { BaseCommandOptions } from "../utils/command"
18
18
  import { createLoggers } from "../utils/effect"
19
19
  import { createProgress } from "../utils/progress"
20
20
  import { withWorktreeLocks } from "./WorktreeLock"
21
- import { VersionControlService } from "./VersionControlService"
21
+ import {
22
+ isDirtyGitStatus,
23
+ VersionControlService,
24
+ } from "./VersionControlService"
22
25
  import type {
23
26
  RegisteredWorkspace,
24
27
  VersionControlBackend,
@@ -75,7 +78,7 @@ interface GitWorktree {
75
78
  readonly branch?: string
76
79
  }
77
80
 
78
- const managedWorktreeIgnorePatterns = ["/.worktree.lock"] as const
81
+ const managedWorktreeIgnorePatterns = ["/.worktree.lock", ".DS_Store"] as const
79
82
  const vcs = "git" as const
80
83
 
81
84
  interface WorktreeOwner {
@@ -550,13 +553,21 @@ const inspectExecution = (
550
553
  const status =
551
554
  actual && actualExists
552
555
  ? yield* fs.runCommand(
553
- ["git", "-C", actual.path, "status", "--porcelain"],
556
+ [
557
+ "git",
558
+ "-C",
559
+ actual.path,
560
+ "status",
561
+ "--porcelain=v1",
562
+ "-z",
563
+ "--untracked-files=all",
564
+ ],
554
565
  { captureOutput: true },
555
566
  )
556
567
  : null
557
568
  const dirty = status
558
569
  ? status.exitCode === 0
559
- ? status.stdout.length > 0
570
+ ? isDirtyGitStatus(status.stdout)
560
571
  : null
561
572
  : null
562
573
  const expectedRef = "branch" in checkout ? checkout.branch : checkout.ref
@@ -2076,7 +2087,15 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
2076
2087
  }
2077
2088
  if (checkoutExists) {
2078
2089
  const status = yield* fs.runCommand(
2079
- ["git", "-C", checkoutPath, "status", "--porcelain"],
2090
+ [
2091
+ "git",
2092
+ "-C",
2093
+ checkoutPath,
2094
+ "status",
2095
+ "--porcelain=v1",
2096
+ "-z",
2097
+ "--untracked-files=all",
2098
+ ],
2080
2099
  { captureOutput: true },
2081
2100
  )
2082
2101
  if (status.exitCode !== 0) {
@@ -2084,7 +2103,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
2084
2103
  message: `Failed to remove worktree for '${alias}': checkout cleanliness could not be verified: ${status.stderr.trim() || `git status exited with code ${status.exitCode}`}`,
2085
2104
  })
2086
2105
  }
2087
- if (status.stdout.trim()) {
2106
+ if (isDirtyGitStatus(status.stdout)) {
2088
2107
  return yield* new WorktreeError({
2089
2108
  message: `Failed to remove worktree for '${alias}': checkout has uncommitted changes`,
2090
2109
  })