@vegastack/skills 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": "@vegastack/skills",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Installer and verifier for VegaStack Agent Skills (Claude Code, Codex, and Hermes)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,18 +17,18 @@ When the operator directly asks in chat for a change, their words are the approv
17
17
 
18
18
  - Run the deterministic guard first: `node <path-to-this-skill>/scripts/preflight.mjs --issue <n> --me $(gh api user -q .login) --json` (add `--repo <o/r> --dev-md <path>` when running outside the project root) — exit 2 stops you with its reasons (open + `ready` state, approval marker, scope label, plan approval on full-plan, Assumptions section, blockers, assignee, repo match); exit 1 passes with warnings — read them into the ledger. Resume and corrections runs pass `--expect working` / `--expect for-operator`.
19
19
  - Then the judgment checks: read the complete brief plus parent issue and milestone for context; re-verify the brief's touch points against the current code (things drift between approval and execution) — including the version-impact line; volatile dependency claims per `dev-architect`'s verify protocol; a full-plan issue's plan still matches reality. A material decision left open — even outside a formal Assumptions section — or reality contradicting brief or plan is a stop: one `handback` comment with the smallest question, `needs-operator`.
20
- - Resuming a dead session's issue: the operator's explicit handover is required; then follow the resume protocol in [ledger-and-resume](references/ledger-and-resume.md) — brief → plan → ledger → `git log`, nothing else.
20
+ - Resuming a dead session's issue: the operator's explicit handover is required; then follow the resume protocol in [ledger-and-resume](references/ledger-and-resume.md) — brief → plan → ledger → `git log`, nothing else. When the claim is instead being abandoned (nothing worth resuming), the operator releases it with `node <path-to-this-skill>/scripts/reclaim.mjs --issue <n>` (`working` → `ready`, unassign; refuses a still-fresh ledger unless `--force`) — never an automatic reclaim.
21
21
 
22
22
  ## Claim
23
23
 
24
- Assign yourself, swap `ready` → `working`, branch from the default branch per dev.md's `branch:` knob, and **create the ledger comment as your first write**. Record each task's base sha before starting it.
24
+ One session owns one issue, end to end — never hold two `working` claims at once. Assign yourself, swap `ready` → `working`, branch from the default branch per dev.md's `branch:` knob, and **create the ledger comment as your first write** — that comment's edit time is this claim's only liveness signal (dev-status reads a long-silent ledger as a possibly-orphaned claim), so a claim whose session dies before writing it is invisible. Record each task's base sha before starting it.
25
25
 
26
26
  ## Build — dark, test-first, checkpointed
27
27
 
28
28
  No progress updates, no questions. A `fix:` issue's diagnosis runs under the `dev-debug` skill — its phases govern the investigation inside this dark mode, and its winning suspect feeds the evidence comment. A spike the brief flagged runs first — its result opens the evidence comment and shapes the rest of the build. Then work the plan task by task:
29
29
 
30
30
  - **Red before green.** Write the failing test first — at the seams the brief names, never elsewhere — watch it fail for the stated reason, implement the minimal code, watch it pass. One slice at a time. The tests-are-real rubric (implementation-coupled, tautological, horizontal-sliced — defined in `dev-review`'s dispatch prompts) applies to your own tests before a reviewer ever sees them.
31
- - **Checkpoint the ledger** after every task (tick the plan checkbox in the same pass) and at every ruling, per the reference.
31
+ - **Checkpoint the ledger** after every task — and in the same pass tick the matching `[x]` in the *plan* comment (a second write, to a different comment than the ledger, and the one the operator's progress view reads — so the easiest to skip and the one that silently rots). Checkpoint at every ruling too; each checkpoint is also the claim's heartbeat, so never batch them "for later," per the reference.
32
32
  - **Transitory artifacts** — subagent reports, scratch diffs, drafts — live in `.vegastack/.tmp/<issue>-<slug>/`; subagents write full output to files there and return short status.
33
33
  - Decide routine things yourself and ledger the rulings. A structural choice mid-build — a new dependency, table, or service — checks `dev-architect`'s trigger discipline first; a moving part with no named trigger is a stop condition.
34
34
  - **The scope ratchet is a stop condition:** work revealed bigger than the issue's scope class (or plainly exceeding one session) → one `handback` comment proposing the upgrade or split (dev-plan's ratchet rules), `needs-operator`, stop.
@@ -71,7 +71,7 @@ Run the `dev-review` skill per dev.md's `review:` knob — fresh subagent axes b
71
71
  Branch: <name> @ <sha7>
72
72
  ```
73
73
 
74
- The tail's sha stays bare: GitHub auto-links a 7-character sha mid-comment as soon as the commit is on the remote, so the tail becomes a live commit link the moment the branch is pushed — while an explicit `/commit/<sha>` link written at hand-back points at a commit that has not been pushed yet, and 404s. The `**Review:**` line is the one home of surfaced rulings: every ledger `Ruling:` appears there, in the order made. Run `node <path-to-this-skill>/scripts/evidence-check.mjs --file <draft> --json` before posting — exit 2 means the shape is incomplete; fix, don't post. Post it, swap `working` → `for-operator`, stop, and close with the plain-language summary (which repeats, never replaces, the evidence content): what was built, which paths were taken, the rulings, what's worth the operator double-checking.
74
+ The tail's sha stays bare: GitHub auto-links a 7-character sha mid-comment as soon as the commit is on the remote, so the tail becomes a live commit link the moment the branch is pushed — while an explicit `/commit/<sha>` link written at hand-back points at a commit that has not been pushed yet, and 404s. The `**Review:**` line is the one home of surfaced rulings: every ledger `Ruling:` appears there, in the order made. Run `node <path-to-this-skill>/scripts/evidence-check.mjs --file <draft> --issue <n> --json` before posting — it checks the draft's shape and, with `--issue`, that the plan comment's `[x]` boxes reflect the ledger's completed tasks; exit 2 means the shape is incomplete or the checkboxes lag the ledger — fix, don't post. Post it, swap `working` → `for-operator`, stop, and close with the plain-language summary (which repeats, never replaces, the evidence content): what was built, which paths were taken, the rulings, what's worth the operator double-checking.
75
75
 
76
76
  ## Corrections loop — code and docs move together
77
77
 
@@ -6,16 +6,18 @@ The ledger comment's format, line vocabulary, and the resume read-order (brief
6
6
 
7
7
  Create the ledger comment as the session's **first write after claiming** — before any code — with just the marker and heading. Then checkpoint, editing in place:
8
8
 
9
- - **After each plan task completes** — and tick the matching checkbox in the plan comment in the same pass. Record the task's base sha *before* starting it, so the `complete` line's commit range is exact.
9
+ - **After each plan task completes** — and tick the matching `[x]` in the plan comment in the same pass. That box is a second write, to a different comment, that your own resume path never reads — so it is the one that silently lags reality, while the operator's progress view depends on it. The hand-back guard (`evidence-check --issue`) blocks when the ledger's completed tasks outnumber the plan's checked boxes. Record the task's base sha *before* starting it, so the `complete` line's commit range is exact.
10
10
  - **After each review fix round**, with the addressed/open counts.
11
11
  - **At every dark-mode judgment call.** A ruling is any decision the brief/plan didn't make for you that a reviewer or the operator could reasonably question. Rulings are cheap; unrecorded decisions are debt.
12
12
  - **On findings deferred or parked at review**, per dev-review's adjudication lines.
13
13
 
14
14
  Never batch checkpoints "for later" — the ledger's value is exactly that a crash between checkpoints loses one task, not the map. Under concurrent edits, last-writer-wins on one comment is accepted (single-operator workflow); note a clobber if you ever see one.
15
15
 
16
+ The ledger's edit time is also this claim's **heartbeat** — the only liveness signal an agent session exposes. dev-status reads a ledger silent past the orphan threshold (6h) as a *possibly-orphaned* claim: the session likely died before hand-back. A session that runs for days but keeps checkpointing never trips it; a dead one's ledger freezes. A single long task can legitimately go quiet — so checkpoint at rulings within it too, keeping the pulse alive — and the flag is always the operator's to act on (check, resume, or reclaim), never an automatic reset.
17
+
16
18
  ## Resuming — dev-implement's additions to the protocol
17
19
 
18
- - The takeover of a `working` issue requires the operator's explicit handover word; the protocol never makes claiming automatic.
20
+ - The takeover of a `working` issue requires the operator's explicit handover word; the protocol never makes claiming automatic. When nothing is worth resuming, the operator releases the claim instead with `scripts/reclaim.mjs --issue <n>` — `working` → `ready`, unassigned — which refuses a ledger still fresh under the orphan threshold unless `--force`.
19
21
  - Corroborate, don't re-verify: the commits the ledger names should exist in `git log` — reconcile the ranges; a mismatch is a `handback`, not a guess.
20
22
  - A task whose last line is a fix round is mid-loop — resume at the next round with the open findings; a later `complete` line supersedes earlier rounds.
21
23
  - Recorded rulings bind the resumed session: build on them, and surface disagreement in the evidence comment instead of re-litigating.
@@ -7,7 +7,7 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { resolve } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { parseFlags, parseMarker, renderResult } from './lib/gh.mjs';
10
+ import { GhUnavailable, findMarkerComment, ghJson, parseFlags, parseMarker, renderResult } from './lib/gh.mjs';
11
11
 
12
12
  const REQUIRED_SECTIONS = [
13
13
  [/\*\*Done:\*\*/, '**Done:** section'],
@@ -38,18 +38,66 @@ export function checkEvidence(text) {
38
38
  return { blocks, warns: [] };
39
39
  }
40
40
 
41
+ // The plan comment's checkboxes and the ledger's completed tasks are two records
42
+ // of the same fact, and the checkbox — a second write, to a different comment
43
+ // than the ledger the session already maintains — is the one that silently rots.
44
+ // At hand-back the ledger's completed tasks must be reflected in the plan's
45
+ // checkboxes, or the operator reads a false x/y. Distinct task numbers with a
46
+ // `complete` line in the ledger, versus `[x]` boxes in the plan.
47
+ export function checkTaskConsistency(comments) {
48
+ const blocks = [];
49
+ const warns = [];
50
+ const plan = findMarkerComment(comments, 'plan')?.comment?.body;
51
+ const ledger = findMarkerComment(comments, 'ledger')?.comment?.body;
52
+
53
+ // No plan comment, or a plan with no checkboxes (e.g. a research issue), is
54
+ // nothing to reconcile — not a violation. No ledger yet is the same.
55
+ if (!plan || !ledger) return { blocks, warns };
56
+ const planDone = (plan.match(/^-\s*\[x\]/gim) ?? []).length;
57
+ const planTotal = planDone + (plan.match(/^-\s*\[ \]/gm) ?? []).length;
58
+ if (planTotal === 0) return { blocks, warns };
59
+
60
+ const completeTasks = new Set();
61
+ for (const m of ledger.matchAll(/^-\s*Task\s+(\d+):\s*complete\b/gim)) completeTasks.add(m[1]);
62
+ const ledgerComplete = completeTasks.size;
63
+
64
+ if (ledgerComplete > planDone) {
65
+ blocks.push(`${ledgerComplete} task(s) are marked complete in the ledger but only ${planDone} of ${planTotal} are checked off in the plan comment — tick the matching [x] boxes before hand-back`);
66
+ }
67
+ return { blocks, warns };
68
+ }
69
+
70
+ // Fetch the issue's comments and reconcile plan checkboxes against the ledger.
71
+ // Fails closed: an unreachable gh blocks rather than passing silently.
72
+ function taskConsistencyFromIssue({ issue, repo: repoFlag }) {
73
+ const repo = repoFlag || ghJson(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
74
+ const comments = ghJson(['api', `repos/${repo}/issues/${issue}/comments`, '--paginate']);
75
+ return checkTaskConsistency(comments);
76
+ }
77
+
41
78
  const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
42
79
  if (invokedDirectly) {
43
80
  const flags = parseFlags(process.argv.slice(2));
44
81
  let outcome;
45
82
  if (!flags.file) {
46
- outcome = { blocks: ['usage: evidence-check.mjs --file <evidence.md> [--json]'], warns: [] };
83
+ outcome = { blocks: ['usage: evidence-check.mjs --file <evidence.md> [--issue <n> --repo <o/r>] [--json]'], warns: [] };
47
84
  } else {
48
85
  try {
49
86
  outcome = checkEvidence(readFileSync(flags.file, 'utf8'));
50
87
  } catch (error) {
51
88
  outcome = { blocks: [`cannot read evidence: ${error.message}`], warns: [] };
52
89
  }
90
+ // Consistency check only when an issue is named — keeps the file-shape check
91
+ // network-free by default. A gh failure fails closed onto the block list.
92
+ if (flags.issue) {
93
+ try {
94
+ const consistency = taskConsistencyFromIssue(flags);
95
+ outcome.blocks.push(...consistency.blocks);
96
+ outcome.warns.push(...consistency.warns);
97
+ } catch (error) {
98
+ outcome.blocks.push(error instanceof GhUnavailable ? `cannot verify plan/ledger consistency: ${error.message}` : `consistency check error: ${error.message}`);
99
+ }
100
+ }
53
101
  }
54
102
  const { exitCode, text } = renderResult('evidence-check', outcome, { json: Boolean(flags.json) });
55
103
  console.log(text);
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // dev-implement reclaim: release an orphaned claim. The operator runs this after
3
+ // dev-status flags a working issue as possibly-orphaned — it resets working →
4
+ // ready and unassigns, so a fresh session can claim it cleanly. It NEVER claims
5
+ // or resumes: a takeover still needs the operator's explicit handover to a new
6
+ // session. It fails closed and refuses to release a claim whose ledger is still
7
+ // fresh (the session may be alive) unless --force is passed.
8
+ //
9
+ // Exit codes: 0 released · 2 refused/blocked (reasons printed). Read-verify runs
10
+ // before any mutation, so a block never leaves a half-released issue.
11
+ // Usage: node reclaim.mjs --issue <n> [--repo o/r] [--orphan-hours 6] [--force] [--json]
12
+ import { execFileSync } from 'node:child_process';
13
+ import { resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { GhUnavailable, findMarkerComment, ghJson, parseFlags, renderResult } from './lib/gh.mjs';
16
+
17
+ const WORKING = 'working';
18
+ const READY = 'ready';
19
+
20
+ const ageHours = (iso, now) => Math.floor((now - Date.parse(iso)) / 3_600_000);
21
+
22
+ // Read-verify: the deterministic facts that must hold before a release. Returns
23
+ // { blocks, plan } — plan is the mutation to run when blocks is empty.
24
+ export function evaluateReclaim({ issue, comments, orphanHours = 6, force = false, now = Date.now() }) {
25
+ const blocks = [];
26
+ const labels = (issue.labels ?? []).map((l) => l.name);
27
+
28
+ if (issue.state && issue.state !== 'open') blocks.push(`issue is ${issue.state} — only an open working issue can be reclaimed`);
29
+ if (!labels.includes(WORKING)) blocks.push(`issue is not '${WORKING}' (labels: [${labels.join(', ') || 'none'}]) — nothing to reclaim`);
30
+
31
+ const moved = findMarkerComment(comments, 'ledger')?.comment?.updated_at ?? null;
32
+ const ageH = moved ? ageHours(moved, now) : null;
33
+ if (!force && ageH !== null && ageH < orphanHours) {
34
+ blocks.push(`ledger moved ${ageH}h ago (< orphan threshold ${orphanHours}h) — this claim may be live; pass --force to release anyway`);
35
+ }
36
+
37
+ const assignees = (issue.assignees ?? []).map((a) => a.login);
38
+ return {
39
+ blocks,
40
+ plan: { removeAssignees: assignees, ledgerAgeHours: ageH },
41
+ };
42
+ }
43
+
44
+ function ghRun(args) {
45
+ try {
46
+ return execFileSync(process.env.VSK_GH || 'gh', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
47
+ } catch (error) {
48
+ const stderr = error.stderr?.toString().trim() || error.message;
49
+ throw new GhUnavailable(`gh ${args.join(' ')} failed: ${stderr}`);
50
+ }
51
+ }
52
+
53
+ export function gatherAndReclaim(flags) {
54
+ const repo = flags.repo || ghJson(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
55
+ const issueNumber = flags.issue;
56
+ const raw = ghJson(['api', `repos/${repo}/issues/${issueNumber}`]);
57
+ const comments = ghJson(['api', `repos/${repo}/issues/${issueNumber}/comments`, '--paginate']);
58
+ const orphanRaw = Number(flags['orphan-hours']);
59
+ const { blocks, plan } = evaluateReclaim({
60
+ issue: { state: raw.state, labels: raw.labels, assignees: raw.assignees },
61
+ comments,
62
+ orphanHours: Number.isFinite(orphanRaw) && orphanRaw >= 1 ? orphanRaw : 6,
63
+ force: Boolean(flags.force),
64
+ });
65
+ if (blocks.length > 0) return { blocks, warns: [] };
66
+
67
+ // Mutations, only past a clean read-verify. Label swap first (frees the
68
+ // claim), then unassign, then the operator-visible note.
69
+ ghRun(['issue', 'edit', String(issueNumber), '-R', repo, '--remove-label', WORKING, '--add-label', READY]);
70
+ for (const login of plan.removeAssignees) {
71
+ ghRun(['issue', 'edit', String(issueNumber), '-R', repo, '--remove-assignee', login]);
72
+ }
73
+ const age = plan.ledgerAgeHours === null ? 'no ledger comment was ever written' : `ledger silent ${plan.ledgerAgeHours}h`;
74
+ const note = `**Claim released.** \`${WORKING}\` → \`${READY}\`, unassigned — ${age}. The prior session did not hand back; its branch and ledger stand. Resume it (dev-implement, the operator's handover) or let a fresh session claim it.`;
75
+ ghRun(['issue', 'comment', String(issueNumber), '-R', repo, '--body', note]);
76
+ return { blocks: [], warns: [`released #${issueNumber}: ${WORKING} → ${READY}, unassigned (${age})`] };
77
+ }
78
+
79
+ const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
80
+ if (invokedDirectly) {
81
+ const flags = parseFlags(process.argv.slice(2), ['json', 'force']);
82
+ let outcome;
83
+ if (!flags.issue) {
84
+ outcome = { blocks: ['usage: reclaim.mjs --issue <n> [--repo o/r] [--orphan-hours 6] [--force] [--json]'], warns: [] };
85
+ } else {
86
+ try {
87
+ outcome = gatherAndReclaim(flags);
88
+ } catch (error) {
89
+ outcome = { blocks: [error instanceof GhUnavailable ? `cannot verify: ${error.message}` : `reclaim error: ${error.message}`], warns: [] };
90
+ }
91
+ }
92
+ const { exitCode, text } = renderResult('reclaim', outcome, { json: Boolean(flags.json) });
93
+ console.log(text);
94
+ process.exit(exitCode);
95
+ }
@@ -12,10 +12,10 @@ Nearest neighbors: `dev-chronicle` answers "how did we get here"; this skill ans
12
12
  ## Gather
13
13
 
14
14
  ```
15
- node <path-to-this-skill>/scripts/status.mjs --stale-days 3 --json
15
+ node <path-to-this-skill>/scripts/status.mjs --orphan-hours 6 --json
16
16
  ```
17
17
 
18
- Read-only; it returns the board (open issues per state label with age, scope, risky), task progress `x/y` from plan-comment checkboxes, ledger movement for `working` issues (stale = no ledger edit within `--stale-days`, default 3), open PRs with check state, pending unrecorded `Decision:` proposals, and the last chronicle entry. Exit 2 = cannot verify (offline, unauthenticated) — report the gap plainly and stop; never render a guessed board.
18
+ Read-only; it returns the board (open issues per state label with age, scope, risky), task progress `x/y` from plan-comment checkboxes, ledger movement for `working` issues in hours (`possiblyOrphaned` = the ledger has been silent past `--orphan-hours`, default 6, or was never written — the claim's heartbeat has stopped), open PRs with check state, pending unrecorded `Decision:` proposals, and the last chronicle entry. Exit 2 = cannot verify (offline, unauthenticated) — report the gap plainly and stop; never render a guessed board.
19
19
 
20
20
  ## Render — names, never bare numbers
21
21
 
@@ -26,8 +26,8 @@ Needs you (N):
26
26
  - <linked title> — <state> <age>d: <one line: what it waits for and the word needed>
27
27
  Waiting on plan (N): - <linked title> — needs-plan <age>d
28
28
  Ready to build (N): - <linked title> — <scope>
29
- In flight (N): - <linked title> — working, task <x>/<y>, ledger moved <n>d ago
30
- Stale (N): - <linked title> — working <age>d, ledger silent <n>d → check or reclaim
29
+ In flight (N): - <linked title> — working, task <x>/<y>, ledger moved <n>h ago
30
+ Possibly orphaned (N): - <linked title> — working <age>d, ledger silent <n>hheartbeat stopped; check, resume, or reclaim (`reclaim.mjs --issue <n>`)
31
31
  Open PRs (N): - <linked title> — checks <green|pending-or-red|no-checks>
32
32
  Pending decisions (N): "<gist-plain>" (<linked issue>) — records at that issue's merge
33
33
  Last chronicle chapter: <date> — <title-plain>
@@ -38,9 +38,10 @@ Next: <the single most valuable operator action, and why>
38
38
  - Sections with zero entries are omitted, not rendered empty. A completely quiet board is one line: "Nothing needs you — <n> issues ready for agents, nothing in flight."
39
39
  - `risky` issues get their flag shown inline wherever they appear.
40
40
  - **Next** is one line, chosen not computed-looking: the action that unblocks the most (a plan approval blocking several ready issues beats a lone review).
41
+ - **Possibly orphaned** is the ledger heartbeat gone silent past the orphan window (or never started) — likely a dead session, not certainly one. Surface it with the `reclaim.mjs` command inline; the operator decides (check the session, hand it to a resume, or release the claim). A long-running task that keeps checkpointing never lands here.
41
42
  - <linked title> means a markdown link this report builds around the issue/PR title and its URL; numbers ride inside the link, never stand alone. That governs the references the board itself makes.
42
43
  - `<title-plain>` / `<gist-plain>` are the script's `titlePlain` / `gistPlain` fields — text quoted from elsewhere (a chronicle title, a decision gist) may arrive carrying markdown links, and raw bracket-and-parenthesis markup means nothing in a terminal, so it is quoted with the markup removed rather than relinked.
43
44
 
44
45
  ## Honesty rules
45
46
 
46
- Data comes only from the script; ordering, the wait-reason one-liners, and Next are the skill's judgment — clearly judgment, never dressed as data. A stale `working` issue is a fact to surface, not an accusation: "check or reclaim" is the operator's call (a takeover still needs their explicit handover, per dev-implement). Close with the plain-language summary only when invoked as part of a larger run; standalone, the report IS the summary.
47
+ Data comes only from the script; ordering, the wait-reason one-liners, and Next are the skill's judgment — clearly judgment, never dressed as data. A possibly-orphaned `working` issue is a fact to surface, not an accusation — the ledger heartbeat went silent, which is likely but not certainly a dead session: "check, resume, or reclaim" is the operator's call (a takeover still needs their explicit handover, and `reclaim.mjs` is theirs to run, per dev-implement). Close with the plain-language summary only when invoked as part of a larger run; standalone, the report IS the summary.
@@ -2,7 +2,7 @@
2
2
  // dev-status data gatherer: everything the board report needs, deterministically,
3
3
  // read-only, markers-only. The skill renders; this script never invents state.
4
4
  //
5
- // Usage: node status.mjs [--repo o/r] [--stale-days 3] [--dev-md <path>] --json
5
+ // Usage: node status.mjs [--repo o/r] [--orphan-hours 6] [--dev-md <path>] --json
6
6
  import { execFileSync } from 'node:child_process';
7
7
  import { existsSync, readFileSync } from 'node:fs';
8
8
  import { resolve } from 'node:path';
@@ -46,6 +46,15 @@ export function ageDays(iso, now = Date.now()) {
46
46
  return Math.floor((now - Date.parse(iso)) / 86_400_000);
47
47
  }
48
48
 
49
+ // Ledger liveness is measured in hours, not days: a session that hands back in
50
+ // hours can go dark for a fraction of a day, which whole-day granularity cannot
51
+ // even represent. The ledger's updated_at is the only liveness proxy an agent
52
+ // session exposes — a live session (even a multi-day one) checkpoints and keeps
53
+ // this small; a dead one freezes it.
54
+ export function ageHours(iso, now = Date.now()) {
55
+ return Math.floor((now - Date.parse(iso)) / 3_600_000);
56
+ }
57
+
49
58
  export function parseMarker(body) {
50
59
  const match = /<!--\s*vsk:v1\s+([^>]*?)\s*-->/.exec(body ?? '');
51
60
  if (!match) return null;
@@ -110,7 +119,7 @@ export function checksState(rollup) {
110
119
  return rollup.every(ok) ? 'green' : 'pending-or-red';
111
120
  }
112
121
 
113
- export function gatherStatus({ repo, staleDays = 3, devMdPath = '.vegastack/dev.md', chroniclePath = '.vegastack/chronicle.md', now = Date.now() } = {}) {
122
+ export function gatherStatus({ repo, orphanHours = 6, devMdPath = '.vegastack/dev.md', chroniclePath = '.vegastack/chronicle.md', now = Date.now() } = {}) {
114
123
  const resolvedRepo = repo || gh(['repo', 'view', '--json', 'nameWithOwner']).nameWithOwner;
115
124
  const devMdText = existsSync(devMdPath) ? readFileSync(devMdPath, 'utf8') : '';
116
125
  const knobs = readKnobs(devMdText);
@@ -133,8 +142,12 @@ export function gatherStatus({ repo, staleDays = 3, devMdPath = '.vegastack/dev.
133
142
  const comments = gh(['api', `repos/${resolvedRepo}/issues/${issue.number}/comments`, '--paginate']);
134
143
  issue.tasks = taskProgress(comments);
135
144
  const moved = ledgerMovedAt(comments);
136
- issue.ledgerAgeDays = moved ? ageDays(moved, now) : null;
137
- issue.stale = bucket === knobs.states[3] && (issue.ledgerAgeDays === null || issue.ledgerAgeDays >= staleDays);
145
+ issue.ledgerAgeHours = moved ? ageHours(moved, now) : null;
146
+ // possiblyOrphaned: a working issue whose ledger has been silent past the
147
+ // orphan threshold — or which never got a ledger comment at all (claimed,
148
+ // then died before its first write). A fact for the operator to act on,
149
+ // never an automatic reclaim: the reset is theirs to run.
150
+ issue.possiblyOrphaned = bucket === knobs.states[3] && (issue.ledgerAgeHours === null || issue.ledgerAgeHours >= orphanHours);
138
151
  decisions.push(...pendingDecisions(comments, registerText).map((d) => ({ issue: issue.number, gist: d, gistPlain: stripLinks(d) })));
139
152
  }
140
153
  }
@@ -151,7 +164,7 @@ export function gatherStatus({ repo, staleDays = 3, devMdPath = '.vegastack/dev.
151
164
  if (m) lastChronicle = { date: m[1], title: m[2], titlePlain: stripLinks(m[2]) };
152
165
  }
153
166
 
154
- return { repo: resolvedRepo, staleDays, board, prs, pendingDecisions: decisions, lastChronicle };
167
+ return { repo: resolvedRepo, orphanHours, board, prs, pendingDecisions: decisions, lastChronicle };
155
168
  }
156
169
 
157
170
  const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
@@ -159,8 +172,8 @@ if (invokedDirectly) {
159
172
  const argv = process.argv.slice(2);
160
173
  const get = (f) => { const i = argv.indexOf(f); return i === -1 ? undefined : argv[i + 1]; };
161
174
  try {
162
- const staleDaysRaw = Number(get('--stale-days'));
163
- const data = gatherStatus({ repo: get('--repo'), staleDays: Number.isFinite(staleDaysRaw) && staleDaysRaw >= 1 ? staleDaysRaw : 3, devMdPath: get('--dev-md') });
175
+ const orphanRaw = Number(get('--orphan-hours'));
176
+ const data = gatherStatus({ repo: get('--repo'), orphanHours: Number.isFinite(orphanRaw) && orphanRaw >= 1 ? orphanRaw : 6, devMdPath: get('--dev-md') });
164
177
  console.log(JSON.stringify(data, null, argv.includes('--json') ? 2 : 0));
165
178
  } catch (error) {
166
179
  console.error(`status: cannot verify — ${error.message}`);
@@ -48,15 +48,16 @@
48
48
  "group": "dev-skills",
49
49
  "repoOnly": false,
50
50
  "files": {
51
- "SKILL.md": "f79f4d312f6c03ffdcf2e2fd44169e48520445900389e2d469c0946811be3801",
51
+ "SKILL.md": "1b38469b3fc0feedf8650ad1f9bfda35485406dc81de89a66e5ab57c670bc798",
52
52
  "agents/openai.yaml": "1abab383deca54d32749fd9156f9a6211fe96e02e0166dc8a16ff8459ac31b74",
53
53
  "references/conventions.md": "95bf916cc11f95f3d5eefcb34db4fbc05d88ae1c8f08869800dfa417f8da17ed",
54
- "references/ledger-and-resume.md": "5e3deae2af67735940564e04adcbc23c0c0dc86da2f6c80d68b8c1207c8f5900",
54
+ "references/ledger-and-resume.md": "974fe4895973aca4ce05ad3eca05c71f1bbb518a59027732c459cc3331fb2f98",
55
55
  "refresh/REFRESH.md": "7b854109d897b78b3055bf1ab89517afdca65c12eb65a4d009141e3ee11cf58f",
56
56
  "refresh/sources.json": "7bd883e5b3512e236a909ad6ff20c97f3555785867c31fd728256c7c2c514c74",
57
- "scripts/evidence-check.mjs": "9a360e6c5460d46b271b28457ee6be92f165c54c771c2dc6860c4a52e6890a9a",
57
+ "scripts/evidence-check.mjs": "974e343ae5372e8b3c930d4ba05e48b29ea5204048f9d2c3cdc8b89237933d8c",
58
58
  "scripts/lib/gh.mjs": "d2261e4b3e2d7b3cdc58123b1503c0264664d7e5a258e90d0caa79333466975a",
59
- "scripts/preflight.mjs": "1bf47b6d147c8603e28d96f7c81f2b4db61b60a3811190ca96b36d83129820d5"
59
+ "scripts/preflight.mjs": "1bf47b6d147c8603e28d96f7c81f2b4db61b60a3811190ca96b36d83129820d5",
60
+ "scripts/reclaim.mjs": "fea8299765ded7052a724e55399f40a1769d91f218df7c38922859800065fa73"
60
61
  }
61
62
  },
62
63
  "dev-intake": {
@@ -134,12 +135,12 @@
134
135
  "group": "dev-skills",
135
136
  "repoOnly": false,
136
137
  "files": {
137
- "SKILL.md": "56415c2f69bb1a2fcb488b9846a20e632eb902b05c5b2b58ae9206d3eaffd77b",
138
+ "SKILL.md": "1e4742805f8f6c99f36e3eea4171b39e336d14aad001d5309b507bf6efa40ae7",
138
139
  "agents/openai.yaml": "72435947f5afef0c8a2c170e987b14fe6057b174318c16b6b538ae8d129791fb",
139
140
  "references/conventions.md": "95bf916cc11f95f3d5eefcb34db4fbc05d88ae1c8f08869800dfa417f8da17ed",
140
141
  "refresh/REFRESH.md": "52aee9bae64d8342e127704d1db282e43ed57c101148727bf10172d6cb975bfa",
141
142
  "refresh/sources.json": "a7ba2340964dc70410626087686145b5790a0e763a18ef2c4d9aa37e950f2e04",
142
- "scripts/status.mjs": "bc56d0b5d996676cfccb752d9d77d0e9e7d64932741cd39d6a503e9761fbe34d"
143
+ "scripts/status.mjs": "f6672ef38cbe31a2335633a96567977d9b885390f1f766bf63f6dd7d172c987b"
143
144
  }
144
145
  },
145
146
  "skill-maintainer": {