@picoai/tickets 0.1.0 → 0.2.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.
@@ -31,9 +31,9 @@ This repository uses a repo-native ticketing system designed for **parallel, lon
31
31
  **TICKETS.md ** explains the workflow, file formats, and required tool usage for both humans and agents. If there is ever a conflict between this file and other docs, follow this file.
32
32
 
33
33
  ## Spec version
34
- - `version`: 1
35
- - `version_url`: `version/20260205-tickets-spec.md`
36
- - Local file: `/.tickets/spec/version/20260205-tickets-spec.md`
34
+ - `version`: 2
35
+ - `version_url`: `version/20260311-tickets-spec.md`
36
+ - Local file: `/.tickets/spec/version/20260311-tickets-spec.md`
37
37
 
38
38
  Version definitions live under `/.tickets/spec/version/`. Each spec file is self-contained and ends with a diff from the previous version.
39
39
 
@@ -63,7 +63,7 @@ This system addresses those problems with stable `ticket.md` files, merge-friend
63
63
  ### Initialize
64
64
  Create the repo structure and templates (idempotent):
65
65
  - `npx @picoai/tickets init`
66
- - Add `--examples` to generate example tickets (7 sample tickets with required/optional fields, relationships, and logs).
66
+ - Add `--examples` to generate example tickets (7 sample tickets with required/optional fields, relationships, and logs that validate under the current spec).
67
67
  - Add `--apply` to upsert/create the managed `## Ticketing Workflow` block in `AGENTS.md` from `AGENTS_EXAMPLE.md` (without creating `AGENTS_EXAMPLE.md` in the target repo).
68
68
  - With `--apply`, `TICKETS.md` updates are marker-scoped: the managed block is replaced and metadata refreshed, while user-owned sections remain unchanged.
69
69
 
@@ -94,14 +94,25 @@ If validation fails and you want a complete report + repair plan:
94
94
  - `npx @picoai/tickets validate --issues --all-fields > issues.yaml`
95
95
  - `npx @picoai/tickets repair --issues-file issues.yaml --all-fields --non-interactive`
96
96
 
97
+ Expected handling loop for a ticket:
98
+ - validate the assigned ticket before changing code
99
+ - use `tickets status` when the lifecycle state changes (`todo`, `doing`, `blocked`, `done`, `canceled`)
100
+ - use `tickets log` for run history within a state: progress, checkpoints, blockers, decisions, verification, and handoff notes
101
+ - machine-written work logs must include `--context`; for split child bootstrapping, pair `--created-from <parent-id>` with `--context ...`
102
+ - reuse the same `--run-started` and `--run-id` for entries from the same run when you want a single per-run log file
103
+
97
104
  ### Log your work (human or agent)
98
105
  Use the CLI to write logs whenever possible (merge-friendly, structured, and tooling-validated).
99
106
 
107
+ Actor defaults for both `tickets status` and `tickets log`:
108
+ - `actor_id`: `--actor-id`, else `TICKETS_ACTOR_ID`, else `@${USER|USERNAME}`, else `"unknown"`
109
+ - `actor_type`: `--actor-type`, else `TICKETS_ACTOR_TYPE`, else infer `agent` from `actor_id` prefix `agent:`, infer `human` from prefix `@`, else default to `human`
110
+
100
111
  Agentic tools (including human-invoked tools like Cursor/Windsurf/Codex CLI/Claude Code) SHOULD log with `--machine`:
101
- - `npx @picoai/tickets log --ticket <id> --actor-type agent --actor-id "cursor (human:@alice)" --summary "Implemented validator." --machine`
112
+ - `npx @picoai/tickets log --ticket <id> --summary "Implemented validator." --machine --actor-id "agent:cursor (human:@alice)" --context "Acceptance criteria from ticket" "API schema v2"`
102
113
 
103
114
  Humans can log without machine marker:
104
- - `npx @picoai/tickets log --ticket <id> --actor-type human --actor-id "@alice" --summary "Investigated failing test; will retry tomorrow."`
115
+ - `npx @picoai/tickets log --ticket <id> --summary "Investigated failing test; will retry tomorrow."`
105
116
 
106
117
  ---
107
118
 
@@ -137,8 +148,8 @@ Example:
137
148
  ```md
138
149
  ---
139
150
  id: 0191c2d3-4e5f-7a8b-9c0d-1e2f3a4b5c6d
140
- version: 1
141
- version_url: "version/20260205-tickets-spec.md"
151
+ version: 2
152
+ version_url: "version/20260311-tickets-spec.md"
142
153
  title: "Add tickets validate --issues"
143
154
  status: todo
144
155
  created_at: 2026-01-29T18:42:10Z
@@ -225,7 +236,8 @@ Recommended transitions:
225
236
  - `done` and `canceled` are immutable unless explicitly reopened by a human (set to `doing` and log why).
226
237
 
227
238
  Status updates:
228
- - `npx @picoai/tickets status --ticket <id> --status doing --log`
239
+ - `npx @picoai/tickets status --ticket <id> --status doing --actor-type agent --actor-id "agent:codex"`
240
+ - `tickets status` always appends a machine-written status-change log entry.
229
241
 
230
242
  ---
231
243
 
@@ -254,7 +266,12 @@ Required fields:
254
266
  - `actor_type`: `human|agent`
255
267
  - `actor_id`: string identifier (freeform)
256
268
  - `summary`: short summary string
269
+ - `event_type`: `status|work`
270
+
271
+ Conditional field:
257
272
  - `context`: `[...]` (bullets capturing the context relevant to this run; when splitting, include copied/adapted inputs from the parent)
273
+ - required for machine-written `work` entries
274
+ - optional for `status` entries and human-written logs
258
275
 
259
276
  Optional structured fields (recommended):
260
277
  - `changes`: `{files: [...], commits: [...], prs: [...]}`
@@ -274,7 +291,7 @@ Validation strictness:
274
291
 
275
292
  Example machine-written entry:
276
293
  ```json
277
- {"version":1,"version_url":"version/20260205-tickets-spec.md","ts":"2026-01-29T18:50:00Z","run_started":"20260129T184210.123Z","actor_type":"agent","actor_id":"codex-cli (human:@alice)","summary":"Implemented tickets validate --issues.","context":["Inputs: AC from ticket, API schema v2"],"verification":{"commands":["npx @picoai/tickets validate"],"results":"pass"},"written_by":"tickets"}
294
+ {"version":2,"version_url":"version/20260311-tickets-spec.md","ts":"2026-01-29T18:50:00Z","run_started":"20260129T184210.123Z","actor_type":"agent","actor_id":"agent:codex-cli (human:@alice)","summary":"Implemented tickets validate --issues.","event_type":"work","context":["Acceptance criteria from ticket","API schema v2"],"verification":{"commands":["npx @picoai/tickets validate"],"results":"pass"},"written_by":"tickets"}
278
295
  ```
279
296
 
280
297
  Merge conflict rule (rare):
@@ -287,11 +304,15 @@ To keep state consistent and merge-friendly, agents and agentic tools SHOULD use
287
304
  - Create tickets: `npx @picoai/tickets new`
288
305
  - Validate: `npx @picoai/tickets validate` (or `--issues` for a full report)
289
306
  - Repair: `npx @picoai/tickets repair --issues-file ...`
290
- - Status changes: `npx @picoai/tickets status --log`
307
+ - Status changes: `npx @picoai/tickets status`
291
308
  - Work logs: `npx @picoai/tickets log` (use `--machine` when the entry is tooling-written)
292
309
  - Listing/triage: `npx @picoai/tickets list` (use `--json` for automation)
293
310
 
294
311
  Humans may edit `ticket.md` directly (it is designed for that), but logs should be appended via the CLI whenever feasible.
312
+ Expected command roles:
313
+ - `tickets status` changes canonical lifecycle state and always records that state transition in logs
314
+ - `tickets log` records run details without changing lifecycle state
315
+ - first child handoff after a split should be a `tickets log` entry with both `created_from` and `context`
295
316
 
296
317
  ---
297
318
 
@@ -328,10 +349,15 @@ Given a ticket assignment, an agent must:
328
349
 
329
350
  1. Open the ticket file at `/.tickets/<ticket-id>/ticket.md`.
330
351
  2. Identify acceptance criteria and verification steps from the ticket.
331
- 3. Plan minimally: determine the smallest change set that satisfies the acceptance criteria.
332
- 4. Implement within limits.
333
- 5. Verify using the ticket's verification steps (or reasonable defaults if absent).
334
- 6. Log the run:
352
+ 3. Validate the ticket before proceeding.
353
+ 4. If beginning active work, set status to `doing`.
354
+ 5. Plan minimally: determine the smallest change set that satisfies the acceptance criteria.
355
+ 6. Implement within limits.
356
+ 7. Verify using the ticket's verification steps (or reasonable defaults if absent).
357
+ 8. Record progress with `tickets log`, reusing the same run metadata for the same run when appropriate.
358
+ - if the log is machine-written, include `context`
359
+ - if the ticket was created by splitting a parent, include `created_from` and the copied/adapted handoff bullets in `context`
360
+ 9. If the run changes lifecycle state, use `tickets status` again (`blocked`, `done`, or explicit reassignment/reaffirmation):
335
361
  - Write progress and outcomes to a per-run log file:
336
362
  - `/.tickets/<ticket-id>/logs/<run_started>-<run_id>.jsonl`
337
363
  - Include a `context` field capturing the relevant context for this run (copied/adapted inputs, decisions carried in, and any subticket handoff details).
@@ -409,6 +435,7 @@ repairs:
409
435
  - `npx @picoai/tickets repair --issues-file <path>` applies enabled repairs:
410
436
  - Safe repairs can be non-interactive.
411
437
  - Disruptive repairs (like changing `id`) must have explicit decisions filled in (or require interactive mode).
438
+ - Basic log repairs are supported for missing/invalid `event_type` and invalid or missing `context` on machine-written work logs.
412
439
 
413
440
  ---
414
441
 
@@ -0,0 +1,38 @@
1
+ # Ticket Format Spec (Version 2)
2
+
3
+ - Version: 2
4
+ - Version URL: `version/20260311-tickets-spec.md`
5
+ - Released: 2026-03-11
6
+ - Status: current
7
+
8
+ ## Definition (format only)
9
+ This version defines the ticket and log formats used by this repo. It does not define workflow policy; see `TICKETS.md` for full workflow.
10
+
11
+ ### Ticket front matter (required)
12
+ - `id`: lowercase UUIDv7 string
13
+ - `version`: format version (integer)
14
+ - `version_url`: path to this definition (repo-local, relative to `.tickets/spec/`)
15
+ - `title`: string
16
+ - `status`: `todo|doing|blocked|done|canceled`
17
+ - `created_at`: ISO 8601 UTC timestamp
18
+
19
+ ### Log entry (required)
20
+ - `version`: format version (integer)
21
+ - `version_url`: path to this definition (repo-local, relative to `.tickets/spec/`)
22
+ - `ts`: ISO 8601 UTC timestamp
23
+ - `run_started`: ISO 8601 UTC timestamp
24
+ - `actor_type`: `human|agent`
25
+ - `actor_id`: string
26
+ - `summary`: short string
27
+ - `event_type`: `status|work`
28
+
29
+ ### Log entry (conditional)
30
+ - `context`: non-empty list of strings when `event_type: work` and the entry is machine-written
31
+
32
+ ### Extensions
33
+ - Extensions are repo-local and must live under the `custom` key.
34
+ - Tools should ignore unknown keys under `custom`.
35
+
36
+ ## Diff from previous version
37
+ - Added required `event_type` to log entries.
38
+ - Clarified that `context` is required for machine-written `work` entries, not for every log entry.
package/README.md CHANGED
@@ -24,9 +24,9 @@ This system addresses those problems with stable `ticket.md` files, merge-friend
24
24
 
25
25
  ## Spec Version
26
26
 
27
- - `version`: 1
28
- - `version_url`: `version/20260205-tickets-spec.md`
29
- - Local file in package assets: `.tickets/spec/version/20260205-tickets-spec.md`
27
+ - `version`: 2
28
+ - `version_url`: `version/20260311-tickets-spec.md`
29
+ - Local file in package assets: `.tickets/spec/version/20260311-tickets-spec.md`
30
30
 
31
31
  Version definitions live under `.tickets/spec/version/`. Each spec file is self-contained and ends with a diff from the previous version.
32
32
 
@@ -58,6 +58,22 @@ npx @picoai/tickets init --apply
58
58
 
59
59
  Repo-native ticketing CLI for Markdown-first, append-only ticket workflows.
60
60
 
61
+ ## Release Provenance
62
+
63
+ - Latest npm release: `@picoai/tickets`
64
+ - Published from commit: `74b0378`
65
+ - Append-only release ledger: `packages/tickets/release-history.json`
66
+
67
+ Check current release posture locally:
68
+
69
+ ```bash
70
+ npm run release:status --workspace @picoai/tickets
71
+ ```
72
+
73
+ Recommended process:
74
+ - after an npm publish succeeds, append a new entry to `packages/tickets/release-history.json`
75
+ - use `npm run release:status --workspace @picoai/tickets` to see whether HEAD is ahead of the last recorded npm release and whether the package version still needs a bump
76
+
61
77
  ## Install
62
78
 
63
79
  ```bash
@@ -92,7 +108,7 @@ Initialize ticketing structure and templates.
92
108
  npx @picoai/tickets init [--examples] [--apply]
93
109
  ```
94
110
 
95
- - `--examples`: generate example tickets and logs.
111
+ - `--examples`: generate example tickets and logs that validate under the current spec.
96
112
  - `--apply`: update managed `TICKETS.md` + `AGENTS.md` sections and skip `AGENTS_EXAMPLE.md` output.
97
113
 
98
114
  ### `new`
@@ -149,6 +165,10 @@ Options:
149
165
  - `--non-interactive`
150
166
  - `--all-fields`
151
167
 
168
+ Notes:
169
+ - `repair` fixes ticket-file issues and basic log issues.
170
+ - Basic log repairs cover missing/invalid `event_type` and invalid or missing `context` on machine-written work logs.
171
+
152
172
  ### `status`
153
173
 
154
174
  Update ticket status.
@@ -159,19 +179,30 @@ npx @picoai/tickets status --ticket <ticket> --status <status> [options]
159
179
 
160
180
  Options:
161
181
  - `--status <status>` (`todo|doing|blocked|done|canceled`)
162
- - `--log`
182
+ - `--actor-type <human|agent>`
183
+ - `--actor-id <id>`
184
+ - `--context <items...>`
163
185
  - `--run-id <runId>`
164
186
  - `--run-started <runStarted>`
165
187
 
188
+ Notes:
189
+ - `status` always appends a machine-written status-change log entry.
190
+ - include `--context` when the status transition depends on new context you want preserved in the audit trail.
191
+ - `actor_id` default order: `--actor-id`, `TICKETS_ACTOR_ID`, `@${USER|USERNAME}`, `"unknown"`.
192
+ - `actor_type` default order: `--actor-type`, `TICKETS_ACTOR_TYPE`, inferred from `actor_id` prefix (`agent:` -> `agent`, `@` -> `human`), then `human`.
193
+
166
194
  ### `log`
167
195
 
168
196
  Append a run log entry.
169
197
 
170
198
  ```bash
171
- npx @picoai/tickets log --ticket <ticket> --actor-type <human|agent> --actor-id <id> --summary "<text>" [options]
199
+ npx @picoai/tickets log --ticket <ticket> --summary "<text>" [options]
172
200
  ```
173
201
 
174
202
  Options:
203
+ - `--actor-type <human|agent>`
204
+ - `--actor-id <id>`
205
+ - `--context <items...>`
175
206
  - `--run-id <runId>`
176
207
  - `--run-started <runStarted>`
177
208
  - `--machine`
@@ -181,10 +212,16 @@ Options:
181
212
  - `--blockers <blockers...>`
182
213
  - `--tickets-created <tickets...>`
183
214
  - `--created-from <ticketId>`
184
- - `--context-carried-over <items...>`
185
215
  - `--verification-commands <commands...>`
186
216
  - `--verification-results <results>`
187
217
 
218
+ Notes:
219
+ - `log` records run details without changing ticket lifecycle state.
220
+ - machine-written work logs require at least one `--context` item.
221
+ - for split child bootstrapping, use `--created-from <parent-ticket-id>` together with `--context ...`.
222
+ - `actor_id` default order: `--actor-id`, `TICKETS_ACTOR_ID`, `@${USER|USERNAME}`, `"unknown"`.
223
+ - `actor_type` default order: `--actor-type`, `TICKETS_ACTOR_TYPE`, inferred from `actor_id` prefix (`agent:` -> `agent`, `@` -> `human`), then `human`.
224
+
188
225
  ### `list`
189
226
 
190
227
  List tickets with optional filters.
@@ -202,6 +239,9 @@ Options:
202
239
  - `--text <text>`
203
240
  - `--json`
204
241
 
242
+ Notes:
243
+ - `--text` searches the ticket title and Markdown body content.
244
+
205
245
  ### `graph`
206
246
 
207
247
  Generate dependency graph output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picoai/tickets",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Repo-native ticketing CLI and assets for Markdown-first, append-only ticket workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "src",
13
+ "release-history.json",
13
14
  ".tickets/spec/AGENTS_EXAMPLE.md",
14
15
  ".tickets/spec/version",
15
16
  ".tickets/spec/TICKETS.md",
@@ -18,6 +19,7 @@
18
19
  ],
19
20
  "scripts": {
20
21
  "tickets": "node ./bin/tickets.js",
22
+ "release:status": "node ./src/release-status.js",
21
23
  "test": "node --test",
22
24
  "test:cli": "node --test tests/tickets-cli.test.js",
23
25
  "pack:check": "npm pack --dry-run --cache /tmp/npm-cache-tickets"
@@ -0,0 +1,12 @@
1
+ {
2
+ "package": "@picoai/tickets",
3
+ "releases": [
4
+ {
5
+ "version": "0.1.0",
6
+ "commit": "74b0378",
7
+ "date": "2026-03-11",
8
+ "channel": "latest",
9
+ "notes": "Published to npm"
10
+ }
11
+ ]
12
+ }
package/src/cli.js CHANGED
@@ -40,6 +40,52 @@ function hasErrors(issues) {
40
40
  return issues.some((issue) => issue.severity === "error");
41
41
  }
42
42
 
43
+ function isValidActorType(value) {
44
+ return ["human", "agent"].includes(value);
45
+ }
46
+
47
+ function resolveActorId(explicitActorId) {
48
+ const candidate = explicitActorId ?? process.env.TICKETS_ACTOR_ID;
49
+ if (typeof candidate === "string" && candidate.trim()) {
50
+ return candidate.trim();
51
+ }
52
+
53
+ const localUser = process.env.USER ?? process.env.USERNAME;
54
+ if (typeof localUser === "string" && localUser.trim()) {
55
+ return `@${localUser.trim()}`;
56
+ }
57
+
58
+ return "unknown";
59
+ }
60
+
61
+ function resolveActorType(explicitActorType, actorId) {
62
+ const candidate = explicitActorType ?? process.env.TICKETS_ACTOR_TYPE;
63
+ if (typeof candidate === "string" && candidate.trim()) {
64
+ if (!isValidActorType(candidate.trim())) {
65
+ throw new Error("Invalid actor type. Use one of: human, agent");
66
+ }
67
+ return candidate.trim();
68
+ }
69
+
70
+ if (typeof actorId === "string") {
71
+ if (actorId.startsWith("agent:")) {
72
+ return "agent";
73
+ }
74
+ if (actorId.startsWith("@")) {
75
+ return "human";
76
+ }
77
+ }
78
+
79
+ return "human";
80
+ }
81
+
82
+ function normalizeContextItems(values) {
83
+ if (!Array.isArray(values)) {
84
+ return [];
85
+ }
86
+ return values.map((value) => String(value).trim()).filter(Boolean);
87
+ }
88
+
43
89
  function printIssues(issues) {
44
90
  for (const issue of issues) {
45
91
  const location = issue.ticket_path ?? issue.log ?? "";
@@ -65,11 +111,14 @@ function buildRepairsFromIssues(issues, options = {}) {
65
111
  for (const issue of issues) {
66
112
  const code = issue.code;
67
113
  const ticketPath = issue.ticket_path;
68
- if (!ticketPath) {
114
+ const logLocation = issue.log;
115
+ const logPath = logLocation ? String(logLocation).replace(/:\d+$/, "") : null;
116
+ if (!ticketPath && !logPath) {
69
117
  continue;
70
118
  }
71
119
 
72
- const key = `${code}:${ticketPath}`;
120
+ const targetPath = ticketPath ?? logPath;
121
+ const key = `${code}:${targetPath}`;
73
122
  if (seen.has(key)) {
74
123
  continue;
75
124
  }
@@ -85,10 +134,31 @@ function buildRepairsFromIssues(issues, options = {}) {
85
134
  id: nextId,
86
135
  enabled: false,
87
136
  issue_ids: [issue.id ?? ""],
88
- ticket_path: ticketPath,
89
137
  };
138
+ if (ticketPath) {
139
+ base.ticket_path = ticketPath;
140
+ }
141
+ if (logPath) {
142
+ base.log_path = logPath;
143
+ }
90
144
 
91
- if (code === "MISSING_SECTION") {
145
+ if (["LOG_EVENT_TYPE_MISSING", "LOG_EVENT_TYPE_INVALID"].includes(code)) {
146
+ repairs.push({
147
+ ...base,
148
+ safe: true,
149
+ action: "set_log_event_type",
150
+ params: {},
151
+ optional: false,
152
+ });
153
+ } else if (["CONTEXT_INVALID", "CONTEXT_EMPTY", "CONTEXT_ENTRY_INVALID", "CONTEXT_MISSING"].includes(code)) {
154
+ repairs.push({
155
+ ...base,
156
+ safe: true,
157
+ action: "normalize_log_context",
158
+ params: {},
159
+ optional: false,
160
+ });
161
+ } else if (code === "MISSING_SECTION") {
92
162
  repairs.push({ ...base, safe: true, action: "add_sections", params: {}, optional: false });
93
163
  } else if (["VERSION_MISSING", "VERSION_INVALID"].includes(code)) {
94
164
  repairs.push({
@@ -678,6 +748,7 @@ function generateExampleTickets() {
678
748
  logs: [
679
749
  {
680
750
  summary: "Epic created and split into child tickets.",
751
+ context: ["Parent planning context for Feature Alpha", "Child tickets were split for parallel execution"],
681
752
  tickets_created: ["backend", "frontend", "testing", "docs"],
682
753
  next_steps: ["Coordinate release window", "Monitor blockers"],
683
754
  },
@@ -709,7 +780,7 @@ function generateExampleTickets() {
709
780
  summary: "Scaffolded API and outlined endpoints.",
710
781
  decisions: ["Using UUID primary keys", "Respond with JSON:API style"],
711
782
  created_from: "parent",
712
- context_carried_over: ["Acceptance criteria from parent", "Release target"],
783
+ context: ["Acceptance criteria from parent", "Release target"],
713
784
  },
714
785
  ],
715
786
  },
@@ -732,7 +803,7 @@ function generateExampleTickets() {
732
803
  summary: "Waiting on API responses to stabilize.",
733
804
  blockers: ["Backend contract not finalized"],
734
805
  created_from: "parent",
735
- context_carried_over: ["Design mocks v1.2", "API schema draft"],
806
+ context: ["Design mocks v1.2", "API schema draft"],
736
807
  },
737
808
  ],
738
809
  },
@@ -754,7 +825,7 @@ function generateExampleTickets() {
754
825
  summary: "Outlined E2E scenarios to automate.",
755
826
  next_steps: ["Set up test data fixtures"],
756
827
  created_from: "parent",
757
- context_carried_over: ["Frontend flow chart", "Backend contract v1"],
828
+ context: ["Frontend flow chart", "Backend contract v1"],
758
829
  },
759
830
  ],
760
831
  },
@@ -776,7 +847,7 @@ function generateExampleTickets() {
776
847
  summary: "Preparing outline; waiting on test results.",
777
848
  blockers: ["Integration tests pending"],
778
849
  created_from: "parent",
779
- context_carried_over: ["Feature overview", "Known limitations"],
850
+ context: ["Feature overview", "Known limitations"],
780
851
  },
781
852
  ],
782
853
  },
@@ -797,6 +868,7 @@ function generateExampleTickets() {
797
868
  logs: [
798
869
  {
799
870
  summary: "Drafted release checklist; waiting on test green.",
871
+ context: ["Release checklist draft", "Waiting on integration test completion"],
800
872
  next_steps: ["Book release window"],
801
873
  },
802
874
  ],
@@ -818,6 +890,7 @@ function generateExampleTickets() {
818
890
  logs: [
819
891
  {
820
892
  summary: "Blocked until backend fix lands.",
893
+ context: ["Regression repro identified", "Awaiting backend deployment before retry"],
821
894
  blockers: ["Awaiting backend deployment"],
822
895
  },
823
896
  ],
@@ -886,16 +959,17 @@ function generateExampleTickets() {
886
959
  actor_type: "agent",
887
960
  actor_id: "tickets-init",
888
961
  summary: logSpec.summary,
962
+ event_type: "work",
889
963
  written_by: "tickets",
890
964
  };
891
965
 
892
966
  for (const key of [
967
+ "context",
893
968
  "decisions",
894
969
  "next_steps",
895
970
  "blockers",
896
971
  "tickets_created",
897
972
  "created_from",
898
- "context_carried_over",
899
973
  ]) {
900
974
  if (!(key in logSpec)) {
901
975
  continue;
@@ -935,8 +1009,11 @@ async function cmdInit(options) {
935
1009
  const versionDir = path.join(repoBaseDir, "version");
936
1010
  ensureDir(versionDir);
937
1011
 
938
- const specPath = path.join(versionDir, "20260205-tickets-spec.md");
939
- writeTemplateFile(specPath, path.join(".tickets", "spec", "version", "20260205-tickets-spec.md"), apply);
1012
+ const currentSpecPath = path.join(versionDir, "20260311-tickets-spec.md");
1013
+ writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "20260311-tickets-spec.md"), apply);
1014
+
1015
+ const previousSpecPath = path.join(versionDir, "20260205-tickets-spec.md");
1016
+ writeTemplateFile(previousSpecPath, path.join(".tickets", "spec", "version", "20260205-tickets-spec.md"), apply);
940
1017
 
941
1018
  const proposedPath = path.join(versionDir, "PROPOSED-tickets-spec.md");
942
1019
  writeTemplateFile(proposedPath, path.join(".tickets", "spec", "version", "PROPOSED-tickets-spec.md"), apply);
@@ -1085,28 +1162,37 @@ async function cmdValidate(options) {
1085
1162
  async function cmdStatus(options) {
1086
1163
  const ticketPath = resolveTicketPath(options.ticket);
1087
1164
  const [frontMatter, body] = loadTicket(ticketPath);
1165
+ const previousStatus = frontMatter.status;
1166
+ const actorId = resolveActorId(options.actorId);
1167
+ const actorType = resolveActorType(options.actorType, actorId);
1168
+ const context = normalizeContextItems(options.context);
1088
1169
 
1089
1170
  frontMatter.status = options.status;
1090
1171
  writeTicket(ticketPath, frontMatter, body);
1091
1172
 
1092
- if (options.log) {
1093
- const runId = options.runId || newUuidv7();
1094
- const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
1095
- const entry = {
1096
- version: FORMAT_VERSION,
1097
- version_url: FORMAT_VERSION_URL,
1098
- ts: iso8601(nowUtc()),
1099
- run_started: runStarted,
1100
- actor_type: "human",
1101
- actor_id: "status-change",
1102
- summary: `Status set to ${options.status}`,
1103
- written_by: "tickets",
1104
- };
1105
-
1106
- const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
1107
- appendJsonl(logPath, entry);
1173
+ const runId = options.runId || newUuidv7();
1174
+ const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
1175
+ const entry = {
1176
+ version: FORMAT_VERSION,
1177
+ version_url: FORMAT_VERSION_URL,
1178
+ ts: iso8601(nowUtc()),
1179
+ run_started: runStarted,
1180
+ actor_type: actorType,
1181
+ actor_id: actorId,
1182
+ summary:
1183
+ previousStatus === options.status
1184
+ ? `Status reaffirmed as ${options.status}`
1185
+ : `Status changed from ${previousStatus ?? "unknown"} to ${options.status}`,
1186
+ event_type: "status",
1187
+ written_by: "tickets",
1188
+ };
1189
+ if (context.length > 0) {
1190
+ entry.context = context;
1108
1191
  }
1109
1192
 
1193
+ const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
1194
+ appendJsonl(logPath, entry);
1195
+
1110
1196
  return 0;
1111
1197
  }
1112
1198
 
@@ -1114,16 +1200,26 @@ async function cmdLog(options) {
1114
1200
  const ticketPath = resolveTicketPath(options.ticket);
1115
1201
  const runId = options.runId || newUuidv7();
1116
1202
  const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
1203
+ const actorId = resolveActorId(options.actorId);
1204
+ const actorType = resolveActorType(options.actorType, actorId);
1205
+ const context = normalizeContextItems(options.context);
1206
+ if (options.machine && context.length === 0) {
1207
+ throw new Error("Machine-written work logs require at least one --context item");
1208
+ }
1117
1209
 
1118
1210
  const entry = {
1119
1211
  version: FORMAT_VERSION,
1120
1212
  version_url: FORMAT_VERSION_URL,
1121
1213
  ts: iso8601(nowUtc()),
1122
1214
  run_started: runStarted,
1123
- actor_type: options.actorType,
1124
- actor_id: options.actorId,
1215
+ actor_type: actorType,
1216
+ actor_id: actorId,
1125
1217
  summary: options.summary,
1218
+ event_type: "work",
1126
1219
  };
1220
+ if (context.length > 0) {
1221
+ entry.context = context;
1222
+ }
1127
1223
 
1128
1224
  if (options.machine) {
1129
1225
  entry.written_by = "tickets";
@@ -1146,9 +1242,6 @@ async function cmdLog(options) {
1146
1242
  if (options.createdFrom) {
1147
1243
  entry.created_from = options.createdFrom;
1148
1244
  }
1149
- if (options.contextCarriedOver?.length) {
1150
- entry.context_carried_over = options.contextCarriedOver;
1151
- }
1152
1245
  if (options.verificationCommands?.length || options.verificationResults) {
1153
1246
  entry.verification = {
1154
1247
  commands: options.verificationCommands || [],
@@ -1226,6 +1319,24 @@ async function cmdRepair(options) {
1226
1319
  autoEnableSafe: !options.interactive,
1227
1320
  }),
1228
1321
  );
1322
+
1323
+ const logsDir = path.join(path.dirname(ticketPath), "logs");
1324
+ if (!fs.existsSync(logsDir)) {
1325
+ continue;
1326
+ }
1327
+ const logFiles = fs
1328
+ .readdirSync(logsDir, { withFileTypes: true })
1329
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
1330
+ .map((entry) => path.join(logsDir, entry.name))
1331
+ .sort((a, b) => a.localeCompare(b));
1332
+ for (const logFile of logFiles) {
1333
+ repairs.push(
1334
+ ...buildRepairsFromIssues(validateRunLog(logFile, false), {
1335
+ includeOptional: options.allFields,
1336
+ autoEnableSafe: !options.interactive,
1337
+ }),
1338
+ );
1339
+ }
1229
1340
  }
1230
1341
 
1231
1342
  const changes = options.interactive
@@ -1364,17 +1475,24 @@ export async function run(argv = process.argv.slice(2)) {
1364
1475
  .description("Update ticket status")
1365
1476
  .requiredOption("--ticket <ticket>")
1366
1477
  .requiredOption("--status <status>")
1367
- .option("--log", "Write a status-change log entry")
1478
+ .option("--actor-type <actorType>")
1479
+ .option("--actor-id <actorId>")
1480
+ .option("--context <items...>")
1368
1481
  .option("--run-id <runId>")
1369
1482
  .option("--run-started <runStarted>")
1370
1483
  .action(async (options) => {
1371
1484
  if (!STATUS_VALUES.includes(options.status)) {
1372
1485
  throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
1373
1486
  }
1487
+ if (options.actorType && !isValidActorType(options.actorType)) {
1488
+ throw new Error("Invalid --actor-type. Use one of: human, agent");
1489
+ }
1374
1490
  process.exitCode = await cmdStatus({
1375
1491
  ticket: options.ticket,
1376
1492
  status: options.status,
1377
- log: Boolean(options.log),
1493
+ actorType: options.actorType,
1494
+ actorId: options.actorId,
1495
+ context: options.context,
1378
1496
  runId: options.runId,
1379
1497
  runStarted: options.runStarted,
1380
1498
  });
@@ -1386,8 +1504,8 @@ export async function run(argv = process.argv.slice(2)) {
1386
1504
  .requiredOption("--ticket <ticket>")
1387
1505
  .option("--run-id <runId>")
1388
1506
  .option("--run-started <runStarted>")
1389
- .requiredOption("--actor-type <actorType>")
1390
- .requiredOption("--actor-id <actorId>")
1507
+ .option("--actor-type <actorType>")
1508
+ .option("--actor-id <actorId>")
1391
1509
  .requiredOption("--summary <summary>")
1392
1510
  .option("--machine")
1393
1511
  .option("--changes <files...>")
@@ -1396,11 +1514,11 @@ export async function run(argv = process.argv.slice(2)) {
1396
1514
  .option("--blockers <blockers...>")
1397
1515
  .option("--tickets-created <tickets...>")
1398
1516
  .option("--created-from <ticketId>")
1399
- .option("--context-carried-over <items...>")
1517
+ .option("--context <items...>")
1400
1518
  .option("--verification-commands <commands...>")
1401
1519
  .option("--verification-results <results>")
1402
1520
  .action(async (options) => {
1403
- if (!["human", "agent"].includes(options.actorType)) {
1521
+ if (options.actorType && !isValidActorType(options.actorType)) {
1404
1522
  throw new Error("Invalid --actor-type. Use one of: human, agent");
1405
1523
  }
1406
1524
  process.exitCode = await cmdLog({
@@ -1417,7 +1535,7 @@ export async function run(argv = process.argv.slice(2)) {
1417
1535
  blockers: options.blockers,
1418
1536
  ticketsCreated: options.ticketsCreated,
1419
1537
  createdFrom: options.createdFrom,
1420
- contextCarriedOver: options.contextCarriedOver,
1538
+ context: options.context,
1421
1539
  verificationCommands: options.verificationCommands,
1422
1540
  verificationResults: options.verificationResults,
1423
1541
  });
@@ -1,6 +1,6 @@
1
1
  export const BASE_DIR = ".tickets/spec";
2
- export const FORMAT_VERSION = 1;
3
- export const FORMAT_VERSION_URL = "version/20260205-tickets-spec.md";
2
+ export const FORMAT_VERSION = 2;
3
+ export const FORMAT_VERSION_URL = "version/20260311-tickets-spec.md";
4
4
 
5
5
  export const STATUS_VALUES = ["todo", "doing", "blocked", "done", "canceled"];
6
6
  export const PRIORITY_VALUES = ["low", "medium", "high", "critical"];
@@ -8,11 +8,11 @@ export function listTickets(filters) {
8
8
  const rows = [];
9
9
 
10
10
  for (const ticketPath of collectTicketPaths(null)) {
11
- const [, frontMatter] = validateTicket(ticketPath);
11
+ const [, frontMatter, body] = validateTicket(ticketPath);
12
12
  if (!frontMatter || Object.keys(frontMatter).length === 0) {
13
13
  continue;
14
14
  }
15
- if (!passesFilters(frontMatter, filters)) {
15
+ if (!passesFilters(frontMatter, body, filters)) {
16
16
  continue;
17
17
  }
18
18
 
@@ -30,7 +30,7 @@ export function listTickets(filters) {
30
30
  return rows;
31
31
  }
32
32
 
33
- function passesFilters(frontMatter, filters) {
33
+ function passesFilters(frontMatter, body, filters) {
34
34
  if (filters.status && frontMatter.status !== filters.status) {
35
35
  return false;
36
36
  }
@@ -51,7 +51,7 @@ function passesFilters(frontMatter, filters) {
51
51
  }
52
52
  if (filters.text) {
53
53
  const text = String(filters.text).toLowerCase();
54
- const haystack = `${frontMatter.title ?? ""}\n${frontMatter.description ?? ""}`.toLowerCase();
54
+ const haystack = `${frontMatter.title ?? ""}\n${body ?? ""}`.toLowerCase();
55
55
  if (!haystack.includes(text)) {
56
56
  return false;
57
57
  }
package/src/lib/repair.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  newUuidv7,
15
15
  nowUtc,
16
16
  parseIso,
17
+ readJsonl,
17
18
  writeTicket,
18
19
  } from "./util.js";
19
20
 
@@ -37,6 +38,7 @@ export async function applyRepairs(repairs, options = {}) {
37
38
  const action = repair.action;
38
39
  const params = repair.params ?? {};
39
40
  const ticketPath = repair.ticket_path;
41
+ const logPath = repair.log_path;
40
42
 
41
43
  if (action === "set_front_matter_field") {
42
44
  const field = params.field;
@@ -91,6 +93,18 @@ export async function applyRepairs(repairs, options = {}) {
91
93
  continue;
92
94
  }
93
95
 
96
+ if (action === "set_log_event_type") {
97
+ setLogEventType(logPath);
98
+ applied.push(`${logPath}: set event_type`);
99
+ continue;
100
+ }
101
+
102
+ if (action === "normalize_log_context") {
103
+ normalizeLogContext(logPath);
104
+ applied.push(`${logPath}: normalized context`);
105
+ continue;
106
+ }
107
+
94
108
  if (nonInteractive) {
95
109
  throw new Error(`Unsupported repair action ${action}`);
96
110
  }
@@ -147,6 +161,7 @@ function describeRepair(repair) {
147
161
  const action = repair.action;
148
162
  const field = repair.params?.field;
149
163
  const ticketPath = repair.ticket_path ?? "";
164
+ const logPath = repair.log_path ?? "";
150
165
  const value = repair.params?.value;
151
166
 
152
167
  if (action === "add_sections") {
@@ -185,6 +200,12 @@ function describeRepair(repair) {
185
200
  if (action === "normalize_verification_commands") {
186
201
  return ["Normalize verification.commands to strings, dropping invalid entries.", null];
187
202
  }
203
+ if (action === "set_log_event_type") {
204
+ return [`Set event_type in ${logPath} based on the log entry shape.`, null];
205
+ }
206
+ if (action === "normalize_log_context") {
207
+ return [`Normalize context in ${logPath} and synthesize a minimal fallback when required.`, null];
208
+ }
188
209
  return ["Apply repair", value];
189
210
  }
190
211
 
@@ -336,3 +357,56 @@ function normalizeVerificationCommands(ticketPath) {
336
357
  };
337
358
  writeTicket(ticketPath, frontMatter, body);
338
359
  }
360
+
361
+ function setLogEventType(logPath) {
362
+ updateJsonl(logPath, (entry) => {
363
+ entry.event_type = inferLogEventType(entry);
364
+ return entry;
365
+ });
366
+ }
367
+
368
+ function normalizeLogContext(logPath) {
369
+ updateJsonl(logPath, (entry) => {
370
+ if (!Array.isArray(entry.context)) {
371
+ entry.context = [];
372
+ }
373
+ entry.context = entry.context.map((value) => String(value).trim()).filter(Boolean);
374
+
375
+ const machineEntry = entry.written_by === "tickets" || entry.machine === true;
376
+ const eventType = inferLogEventType(entry);
377
+ if (entry.event_type !== eventType) {
378
+ entry.event_type = eventType;
379
+ }
380
+ if (machineEntry && eventType === "work" && entry.context.length === 0) {
381
+ entry.context = buildFallbackContext(entry);
382
+ }
383
+ return entry;
384
+ });
385
+ }
386
+
387
+ function buildFallbackContext(entry) {
388
+ const items = [];
389
+ if (typeof entry.created_from === "string" && entry.created_from.trim()) {
390
+ items.push(`Handoff from parent ticket ${entry.created_from.trim()}.`);
391
+ }
392
+ if (typeof entry.summary === "string" && entry.summary.trim()) {
393
+ items.push(`Recovered from summary: ${entry.summary.trim()}`);
394
+ }
395
+ if (items.length === 0) {
396
+ items.push("Recovered context was unavailable during automated repair.");
397
+ }
398
+ return items;
399
+ }
400
+
401
+ function inferLogEventType(entry) {
402
+ if (typeof entry.summary === "string" && /^Status (changed|reaffirmed)\b/.test(entry.summary)) {
403
+ return "status";
404
+ }
405
+ return "work";
406
+ }
407
+
408
+ function updateJsonl(logPath, updateEntry) {
409
+ const entries = readJsonl(logPath);
410
+ const content = entries.map((entry) => JSON.stringify(updateEntry(entry))).join("\n");
411
+ fs.writeFileSync(logPath, `${content}\n`);
412
+ }
package/src/lib/util.js CHANGED
@@ -49,7 +49,11 @@ export function parseIso(value) {
49
49
  if (typeof value !== "string") {
50
50
  return null;
51
51
  }
52
- const ts = Date.parse(value);
52
+ const basicMatch = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\.\d+)?Z$/.exec(value);
53
+ const normalized = basicMatch
54
+ ? `${basicMatch[1]}-${basicMatch[2]}-${basicMatch[3]}T${basicMatch[4]}:${basicMatch[5]}:${basicMatch[6]}${basicMatch[7] ?? ""}Z`
55
+ : value;
56
+ const ts = Date.parse(normalized);
53
57
  return Number.isNaN(ts) ? null : new Date(ts);
54
58
  }
55
59
 
@@ -360,6 +360,7 @@ export function validateRunLog(logPath, machineStrictDefault) {
360
360
  const loc = `${logPath}:${idx + 1}`;
361
361
  const machineEntry =
362
362
  machineStrictDefault || entry.written_by === "tickets" || entry.machine === true;
363
+ let eventType = null;
363
364
 
364
365
  for (const required of ["ts", "run_started", "actor_type", "actor_id", "summary"]) {
365
366
  if (!(required in entry)) {
@@ -372,6 +373,24 @@ export function validateRunLog(logPath, machineStrictDefault) {
372
373
  }
373
374
  }
374
375
 
376
+ if (!("event_type" in entry)) {
377
+ issues.push({
378
+ severity: machineEntry ? "error" : "warning",
379
+ code: "LOG_EVENT_TYPE_MISSING",
380
+ message: "event_type missing",
381
+ log: loc,
382
+ });
383
+ } else if (!["status", "work"].includes(entry.event_type)) {
384
+ issues.push({
385
+ severity: machineEntry ? "error" : "warning",
386
+ code: "LOG_EVENT_TYPE_INVALID",
387
+ message: "event_type must be status|work",
388
+ log: loc,
389
+ });
390
+ } else {
391
+ eventType = entry.event_type;
392
+ }
393
+
375
394
  if (!("version" in entry)) {
376
395
  issues.push({
377
396
  severity: machineEntry ? "error" : "warning",
@@ -459,6 +478,43 @@ export function validateRunLog(logPath, machineStrictDefault) {
459
478
  });
460
479
  }
461
480
 
481
+ if ("context" in entry) {
482
+ if (!Array.isArray(entry.context)) {
483
+ issues.push({
484
+ severity: machineEntry ? "error" : "warning",
485
+ code: "CONTEXT_INVALID",
486
+ message: "context must be a list of strings",
487
+ log: loc,
488
+ });
489
+ } else {
490
+ if (machineEntry && eventType === "work" && entry.context.length === 0) {
491
+ issues.push({
492
+ severity: "error",
493
+ code: "CONTEXT_EMPTY",
494
+ message: "context must contain at least one item for machine-written work logs",
495
+ log: loc,
496
+ });
497
+ }
498
+ for (const item of entry.context) {
499
+ if (typeof item !== "string" || !item.trim()) {
500
+ issues.push({
501
+ severity: machineEntry ? "error" : "warning",
502
+ code: "CONTEXT_ENTRY_INVALID",
503
+ message: "context entries must be non-empty strings",
504
+ log: loc,
505
+ });
506
+ }
507
+ }
508
+ }
509
+ } else if (machineEntry && eventType === "work") {
510
+ issues.push({
511
+ severity: "error",
512
+ code: "CONTEXT_MISSING",
513
+ message: "context required for machine-written work logs",
514
+ log: loc,
515
+ });
516
+ }
517
+
462
518
  if (machineEntry && entry.written_by !== "tickets" && entry.machine !== true) {
463
519
  issues.push({
464
520
  severity: "error",
@@ -0,0 +1,141 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ function packageRoot() {
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ return path.resolve(here, "..");
9
+ }
10
+
11
+ function readJson(filePath) {
12
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
13
+ }
14
+
15
+ function runGit(args) {
16
+ const result = spawnSync("git", args, {
17
+ cwd: packageRoot(),
18
+ encoding: "utf8",
19
+ });
20
+ if (result.status !== 0) {
21
+ throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
22
+ }
23
+ return result.stdout.trim();
24
+ }
25
+
26
+ export function summarizeReleaseStatus({
27
+ packageName,
28
+ currentVersion,
29
+ headCommit,
30
+ dirty,
31
+ latestRelease,
32
+ }) {
33
+ const messages = [];
34
+ const publishedVersion = latestRelease?.version ?? "unknown";
35
+ const publishedCommit = latestRelease?.commit ?? "unknown";
36
+ const sameCommit = latestRelease ? headCommit === latestRelease.commit : false;
37
+ const sameVersion = latestRelease ? currentVersion === latestRelease.version : false;
38
+
39
+ if (!latestRelease) {
40
+ messages.push("No published release is recorded yet.");
41
+ } else if (sameCommit && sameVersion && !dirty) {
42
+ messages.push("HEAD matches the latest recorded npm release.");
43
+ } else {
44
+ if (!sameCommit) {
45
+ messages.push(`HEAD (${headCommit}) is ahead of the latest recorded release commit (${publishedCommit}).`);
46
+ }
47
+ if (!sameVersion) {
48
+ messages.push(
49
+ `Package version (${currentVersion}) differs from the latest recorded release version (${publishedVersion}).`,
50
+ );
51
+ } else if (!sameCommit) {
52
+ messages.push("Package version has not changed since the latest recorded npm release.");
53
+ }
54
+ if (dirty) {
55
+ messages.push("Working tree has uncommitted changes.");
56
+ }
57
+ }
58
+
59
+ let recommendation;
60
+ if (!latestRelease) {
61
+ recommendation = "Record the first published release after npm publish.";
62
+ } else if (dirty) {
63
+ recommendation = "Commit or discard local changes before deciding whether to publish.";
64
+ } else if (!sameCommit && sameVersion) {
65
+ recommendation = "If this work should ship, bump the package version before publishing.";
66
+ } else if (!sameCommit && !sameVersion) {
67
+ recommendation = "If tests and docs are ready, HEAD is a plausible publish candidate.";
68
+ } else {
69
+ recommendation = "No publish action is indicated by release provenance alone.";
70
+ }
71
+
72
+ return {
73
+ packageName,
74
+ currentVersion,
75
+ headCommit,
76
+ dirty,
77
+ latestRelease,
78
+ messages,
79
+ recommendation,
80
+ };
81
+ }
82
+
83
+ export function loadReleaseStatus() {
84
+ const root = packageRoot();
85
+ const packageJson = readJson(path.join(root, "package.json"));
86
+ const history = readJson(path.join(root, "release-history.json"));
87
+ const releases = Array.isArray(history.releases) ? history.releases : [];
88
+ const latestRelease = releases.length > 0 ? releases[releases.length - 1] : null;
89
+ const headCommit = runGit(["rev-parse", "--short", "HEAD"]);
90
+ const dirty = runGit(["status", "--short"]).length > 0;
91
+
92
+ return summarizeReleaseStatus({
93
+ packageName: packageJson.name,
94
+ currentVersion: packageJson.version,
95
+ headCommit,
96
+ dirty,
97
+ latestRelease,
98
+ });
99
+ }
100
+
101
+ export function formatReleaseStatus(status) {
102
+ const lines = [
103
+ `Package: ${status.packageName}`,
104
+ `Current version: ${status.currentVersion}`,
105
+ `Current HEAD: ${status.headCommit}`,
106
+ `Working tree dirty: ${status.dirty ? "yes" : "no"}`,
107
+ ];
108
+
109
+ if (status.latestRelease) {
110
+ lines.push(`Latest recorded npm release: ${status.latestRelease.version}`);
111
+ lines.push(`Latest recorded release commit: ${status.latestRelease.commit}`);
112
+ if (status.latestRelease.date) {
113
+ lines.push(`Latest recorded release date: ${status.latestRelease.date}`);
114
+ }
115
+ } else {
116
+ lines.push("Latest recorded npm release: none");
117
+ }
118
+
119
+ lines.push("");
120
+ lines.push("Status:");
121
+ for (const message of status.messages) {
122
+ lines.push(`- ${message}`);
123
+ }
124
+ lines.push(`- Recommendation: ${status.recommendation}`);
125
+ return `${lines.join("\n")}\n`;
126
+ }
127
+
128
+ async function main() {
129
+ try {
130
+ const status = loadReleaseStatus();
131
+ process.stdout.write(formatReleaseStatus(status));
132
+ process.exitCode = 0;
133
+ } catch (error) {
134
+ process.stderr.write(`${String(error.message ?? error)}\n`);
135
+ process.exitCode = 1;
136
+ }
137
+ }
138
+
139
+ if (import.meta.url === `file://${process.argv[1]}`) {
140
+ await main();
141
+ }