@plot-pm/board 0.17.0 → 0.18.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plot-pm/board",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@ manifest_string() { # $1=manifest $2=field → prints the value, or nothing
48
48
  #
49
49
  # WHY THIS EXISTS. `free = process alive AND manifest names no branch`, and the
50
50
  # second half was unreachable. `seal_declaration` runs the moment a branch is
51
- # done; `update_manifest_on_hop` runs after `--next` answers and a worktree is
51
+ # done; the hop that follows runs after `--next` answers and a worktree is
52
52
  # built. Between those two points the agent genuinely holds no slice and the
53
53
  # manifest still named the last one, so `isFree`'s empty-branch arm — written,
54
54
  # exported and unit-tested since `a-dispatch-asks-for-a-free-agent` — had no
@@ -60,7 +60,7 @@ manifest_string() { # $1=manifest $2=field → prints the value, or nothing
60
60
  # and the liveness check with it, since both are keyed on the worktree path.
61
61
  # `wavesCount` counts hops and no hop has happened yet. The node one-liner
62
62
  # round-trips the whole object, so every other field survives verbatim, the same
63
- # property `update_manifest_on_hop` records.
63
+ # property the hop's own write records.
64
64
  #
65
65
  # ADDED, NOT SUBSTITUTED. The hop still rewrites `branch` and `worktree`
66
66
  # together; this writes the empty value that sits between two slices. A worker
@@ -69,7 +69,7 @@ manifest_string() { # $1=manifest $2=field → prints the value, or nothing
69
69
  #
70
70
  # ABSENT IS NOT A FAILURE. No manifest — a hand-started loop, an older
71
71
  # dispatcher — means there is nothing to clear and nothing to report, so this
72
- # returns 0 like `update_manifest_on_hop` does.
72
+ # returns 0 like the hop's own write does.
73
73
  clear_manifest_branch() { # $1=manifest
74
74
  local manifest="$1"
75
75
  [ -f "$manifest" ] || return 0
@@ -114,13 +114,13 @@ plot_session_id() {
114
114
  # into both, and they part at the first hop to a new branch: `session` names the
115
115
  # agent and its manifest file for the agent's whole life, while `resumeId` names
116
116
  # the current slice's conversation and is what the board joins the transcript
117
- # on. Reading this field rather than `$PLOT_SESSION_ID` is what makes the
118
- # hop's write (`update_manifest_on_hop` in `plot-worker-loop.sh`) mean anything:
119
- # a reader asks for the handle, and gets the one the hop last wrote.
117
+ # on. Reading this field rather than `$PLOT_SESSION_ID` is what makes the hop's
118
+ # write (`writeResumeId` in `manifest-stamp.ts`) mean anything: a reader asks
119
+ # for the handle, and gets the one the hop last wrote.
120
120
  #
121
- # A PARSE FAILURE AND AN ABSENT MANIFEST ARE ONE ANSWER, the shape
122
- # `assigned_branch` in `plot-worker-loop.sh` already takes: no handle. A hand-started loop has no
123
- # manifest, and a manifest nobody can read is not a handle.
121
+ # A PARSE FAILURE AND AN ABSENT MANIFEST ARE ONE ANSWER: no handle. A
122
+ # hand-started loop has no manifest, and a manifest nobody can read is not a
123
+ # handle.
124
124
  manifest_resume_id() { # $1=manifest → prints the handle, or nothing
125
125
  manifest_string "$1" resumeId
126
126
  }
package/plot-config.sh CHANGED
@@ -150,10 +150,12 @@
150
150
  # Agent max turns turns one SDK run may take. Absent = 150.
151
151
  # Slice max runs SDK runs one slice may start. Absent = 12.
152
152
  # Agent context window an SDK run's `autoCompactWindow`. Absent = 200000.
153
- # Agent max spend dollars one SDK run may spend (`maxBudgetUsd`); the
154
- # run then ends `spend-limit`. Absent = no limit, never 0.
155
- # Slice max spend dollars one slice's runs may total, read from the
156
- # slice-spend record before each run. Absent = no limit.
153
+ # Agent max spend dollars one SDK run may spend (`maxBudgetUsd`); the run
154
+ # then ends `spend-limit`. `$` and `USD` read, e.g. `$20`.
155
+ # Slice max spend dollars one slice's runs may total, read from the slice-
156
+ # spend record before each run. Both: absent/blank = no
157
+ # limit; present and unreadable (0, negative, text) refuses
158
+ # the worker's start.
157
159
  # Plot 2 posture keys (repo-declared ceremony bounds; all optional):
158
160
  # Plan PRs required | never | optional (never = hard gate)
159
161
  # Implementation home this repo | <repo/path list> | none
@@ -29,19 +29,12 @@
29
29
  # now points at, so a second occurrence is visible in a log rather than
30
30
  # invisible in a working system.
31
31
  #
32
- # IT DOES NOT REPAIR A SYMREF THAT RESOLVES. Only an unresolvable one is broken.
33
- # A clone whose `origin/HEAD` deliberately names a non-default branch is
34
- # somebody's choice, and `--auto` would silently overrule it.
35
- #
36
- # WHAT LEAVES THE PIN BEHIND IS NOT REPAIRED HERE, and the leave-alone rule is
37
- # what keeps this safe alongside it. `packages/domain/corpus/refs.corpus.test.ts`
38
- # repoints `origin/HEAD` at a `plot-corpus-pin` ref it creates in `beforeAll`
39
- # and restores in `afterAll` — deliberately, so two readings of the estate see
40
- # one world. While that suite runs the pin RESOLVES, so this repairs nothing and
41
- # the suite is unaffected. What was measured on 2026-09-04 is the state after a
42
- # run that never reached its `afterAll`: the symref left behind, the ref it
43
- # names gone. Fixing that belongs to the suite; this repairs the symptom and
44
- # reports it loudly enough that the cause stays findable.
32
+ # IT REPAIRS A SYMREF THAT RESOLVES ONLY WHEN IT NAMES `plot-corpus-pin`. A
33
+ # clone whose `origin/HEAD` deliberately names another non-default branch is
34
+ # somebody's choice, and `--auto` would silently overrule it. No operator
35
+ # chooses the pin: `packages/domain/corpus/refs.corpus.test.ts` pins its own
36
+ # disposable clone, so a symref naming it in any other repository is the
37
+ # leftover of an interrupted run.
45
38
 
46
39
  # Whether `refs/remotes/origin/HEAD` names a ref that exists.
47
40
  #
@@ -51,14 +44,17 @@
51
44
  # Usage: origin_head_resolves [<repo-dir>]
52
45
  # Returns: 0 when it resolves — including when there is no symref at all, which
53
46
  # is a FRESH CLONE rather than a corruption and has nothing to repair.
47
+ # A symref naming `plot-corpus-pin` is corrupt even when it resolves:
48
+ # that name is left behind only by an interrupted corpus run, never by
49
+ # a live one, since the corpus pins its own clone.
54
50
  origin_head_resolves() {
55
51
  local dir="${1:-.}" target
56
52
  target=$(git -C "$dir" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null) || return 0
57
- [ -n "$target" ] || return 0
53
+ case "$target" in '') return 0 ;; refs/remotes/origin/plot-corpus-pin) return 1 ;; esac
58
54
  git -C "$dir" rev-parse --verify --quiet "${target}^{commit}" >/dev/null 2>&1
59
55
  }
60
56
 
61
- # Repairs an unresolvable `refs/remotes/origin/HEAD`, and says what it did.
57
+ # Repairs a `refs/remotes/origin/HEAD` that `origin_head_resolves` refuses, and says what it did.
62
58
  #
63
59
  # The repair is `git remote set-head origin --auto`, which asks the remote. It
64
60
  # is cheap — measured under a second — and it is the same command that fixed
@@ -70,16 +66,16 @@ origin_head_resolves() {
70
66
  # this is a self-heal on a path that has its own refusal downstream,
71
67
  # so it must never become a second way to stop.
72
68
  repair_origin_head() {
73
- local dir="${1:-.}" was now
69
+ local dir="${1:-.}" was why
74
70
  origin_head_resolves "$dir" && return 0
75
71
 
76
72
  was=$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
73
+ why="does not resolve"; [ "$was" != origin/plot-corpus-pin ] || why="is the corpus pin, left by an interrupted corpus run"
77
74
  if ! git -C "$dir" remote set-head origin --auto >/dev/null 2>&1; then
78
- echo "plot: origin/HEAD points at '$was', which does not resolve, and 'git remote set-head origin --auto' failed — the remote could not be asked." >&2
75
+ echo "plot: origin/HEAD points at '$was', which $why, and 'git remote set-head origin --auto' failed — the remote could not be asked." >&2
79
76
  return 0
80
77
  fi
81
- now=$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
82
- echo "plot: repaired origin/HEAD — it pointed at '$was', which does not exist on the remote; it now points at '${now:-<unset>}'." >&2
78
+ echo "plot: repaired origin/HEAD — it pointed at '$was', which $why; it now points at '$(git -C "$dir" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || echo '<unset>')'." >&2
83
79
  return 0
84
80
  }
85
81
 
package/plot-host.sh CHANGED
@@ -3605,7 +3605,8 @@ case "$op" in
3605
3605
  *) die "pr-merged: unknown arg $1" ;;
3606
3606
  esac
3607
3607
  done
3608
- if [ "$be" = "github" ]; then
3608
+ # NO REMOTE, NO PR: no repository holds one, so nothing merged, whatever gh's auth says.
3609
+ if [ ${#repo_args[@]} -eq 0 ] && [ -z "$(git remote 2>/dev/null)" ]; then echo "not-merged"; elif [ "$be" = "github" ]; then
3609
3610
  # --state all, because a merged PR reports CLOSED and the default `open`
3610
3611
  # would hide every one of them. --limit 100 rather than 1: the newest PR
3611
3612
  # is not the merge, exactly as the state is not the merge.
@@ -4508,13 +4509,25 @@ case "$op" in
4508
4509
  # a network failure exits 4 here, the way the Jenkins arm does, so the
4509
4510
  # monitor reads *could not ask* and not *no run yet*.
4510
4511
  _gh_runs=$(gh run list --branch "$branch" --limit "$limit" \
4511
- --json headSha,conclusion,status,startedAt,url 2>/dev/null) \
4512
+ --json headSha,conclusion,status,startedAt,url,databaseId 2>/dev/null) \
4512
4513
  || { echo "plot-host: run-for-sha — gh run list failed for '$branch'" >&2; exit 4; }
4513
- printf '%s' "$_gh_runs" | jq -c --arg sha "$sha" \
4514
+ _gh_match=$(printf '%s' "$_gh_runs" | jq -c --arg sha "$sha" \
4514
4515
  '(map(select(.headSha == $sha)) | .[0]) | select(. != null)
4515
4516
  | {sha:.headSha, status:.status,
4516
4517
  conclusion:(if (.conclusion // "") == "" then null else .conclusion end),
4517
- url:.url, startedAt:.startedAt}' || exit 4
4518
+ url:.url, startedAt:.startedAt, databaseId:.databaseId}') || exit 4
4519
+ [ -n "$_gh_match" ] || exit 0
4520
+ # ONE MORE CALL, ONLY FOR A CONCLUDED FAILURE (#1295): `gh run view --json
4521
+ # jobs` is the only place a 0-step job (no runner ever picked up the run)
4522
+ # shows up, apart from a real failure. A run still going or that
4523
+ # succeeded costs no extra call.
4524
+ _gh_db_id=$(printf '%s' "$_gh_match" | jq -r 'if (.conclusion == "failure" or .conclusion == "cancelled") then .databaseId else "" end')
4525
+ _gh_match=$(printf '%s' "$_gh_match" | jq -c 'del(.databaseId)') || exit 4
4526
+ [ -z "$_gh_db_id" ] && { printf '%s' "$_gh_match"; exit 0; }
4527
+ # Slurped via stdin, never `--argjson`: that flag caps at 128 KB on Linux.
4528
+ _gh_jobs=$(gh run view "$_gh_db_id" --json jobs 2>/dev/null) || { echo "plot-host: run-for-sha — gh run view failed" >&2; exit 4; }
4529
+ jq -n -c --slurpfile match <(printf '%s' "$_gh_match") --slurpfile run <(printf '%s' "$_gh_jobs") \
4530
+ '$match[0] + {jobs: [$run[0].jobs[] | {conclusion, steps: (.steps | length)}]}' || exit 4
4518
4531
  ;;
4519
4532
 
4520
4533
  issue-list)