@picoai/tickets 0.4.0 → 0.5.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/.tickets/spec/AGENTS_EXAMPLE.md +4 -0
- package/.tickets/spec/TICKETS.md +31 -7
- package/.tickets/spec/version/20260205-tickets-spec.md +1 -1
- package/.tickets/spec/version/20260311-tickets-spec.md +1 -1
- package/.tickets/spec/version/20260317-2-tickets-spec.md +1 -1
- package/.tickets/spec/version/20260317-3-tickets-spec.md +121 -0
- package/.tickets/spec/version/20260317-4-tickets-spec.md +120 -0
- package/.tickets/spec/version/20260317-tickets-spec.md +1 -1
- package/README.md +4 -2
- package/package.json +1 -1
- package/release-history.json +7 -0
- package/src/cli.js +185 -8
- package/src/lib/constants.js +3 -1
- package/src/lib/projections.js +4 -0
- package/src/lib/validation.js +236 -0
|
@@ -11,6 +11,10 @@ The purpose of this bootstrap is to ensure an agent loads the canonical ticketin
|
|
|
11
11
|
- When the human uses feature/phase/milestone/roadmap or custom repo terms, keep using their vocabulary in the conversation and translate it into the generic CLI planning fields internally.
|
|
12
12
|
- Use the repo-local CLI (`npx @picoai/tickets`) as the integration surface for tickets and logs.
|
|
13
13
|
- Before performing work on a ticket, validate it: run `npx @picoai/tickets validate` (or `npx @picoai/tickets validate --issues` + `npx @picoai/tickets repair`).
|
|
14
|
+
- Before moving a ticket to `done`, confirm the ticket's `## Acceptance Criteria` are met and its `## Verification` checks passed.
|
|
15
|
+
- If those completion gates are not satisfied, ask the human whether to keep working or explicitly override the gates. Only move the ticket to `done` after that human decision.
|
|
16
|
+
- Record `completion` metadata every time a ticket is moved to `done`.
|
|
17
|
+
- When a human overrides incomplete completion gates, record that override in the ticket via `npx @picoai/tickets status --status done --acceptance-criteria ... --verification-state ... --override-by ... --override-reason ...`.
|
|
14
18
|
- When logging via the CLI: use `npx @picoai/tickets log --machine` so logs are strictly structured.
|
|
15
19
|
- Respect `assignment.mode`, `agent_limits`, active advisory claims, and repo-local defaults in `.tickets/config.yml`.
|
|
16
20
|
|
package/.tickets/spec/TICKETS.md
CHANGED
|
@@ -40,8 +40,8 @@ This repository uses a repo-native ticketing system designed for **parallel, lon
|
|
|
40
40
|
|
|
41
41
|
## Spec version
|
|
42
42
|
- `version`: 3
|
|
43
|
-
- `version_url`: `version/20260317-
|
|
44
|
-
- Local file: `/.tickets/spec/version/20260317-
|
|
43
|
+
- `version_url`: `version/20260317-4-tickets-spec.md`
|
|
44
|
+
- Local file: `/.tickets/spec/version/20260317-4-tickets-spec.md`
|
|
45
45
|
|
|
46
46
|
Version definitions live under `/.tickets/spec/version/`. Each spec file is self-contained and ends with a diff from the previous version.
|
|
47
47
|
|
|
@@ -148,7 +148,7 @@ Treat the list above as defaults. Agents should consult `.tickets/config.yml` be
|
|
|
148
148
|
---
|
|
149
149
|
id: 0191c2d3-4e5f-7a8b-9c0d-1e2f3a4b5c6d
|
|
150
150
|
version: 3
|
|
151
|
-
version_url: "version/20260317-
|
|
151
|
+
version_url: "version/20260317-4-tickets-spec.md"
|
|
152
152
|
title: "Feature Alpha"
|
|
153
153
|
status: doing
|
|
154
154
|
created_at: 2026-03-17T17:00:00Z
|
|
@@ -193,7 +193,7 @@ resolution: dropped
|
|
|
193
193
|
Claim log example:
|
|
194
194
|
|
|
195
195
|
```json
|
|
196
|
-
{"version":3,"version_url":"version/20260317-
|
|
196
|
+
{"version":3,"version_url":"version/20260317-4-tickets-spec.md","ts":"2026-03-17T17:05:00Z","run_started":"20260317T170500.000Z","actor_type":"agent","actor_id":"agent:codex","summary":"Acquired claim 0191c2d3-...","event_type":"claim","written_by":"tickets","claim":{"action":"acquire","claim_id":"0191c2d3-4e5f-7a8b-9c0d-1e2f3a4b5d00","holder_id":"agent:codex","holder_type":"agent","ttl_minutes":60,"expires_at":"2026-03-17T18:05:00Z","reason":""}}
|
|
197
197
|
```
|
|
198
198
|
|
|
199
199
|
## Ticket definition (`ticket.md`)
|
|
@@ -246,6 +246,23 @@ Resolution:
|
|
|
246
246
|
resolution: completed # completed | merged | dropped
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
Completion:
|
|
250
|
+
```yaml
|
|
251
|
+
completion:
|
|
252
|
+
acceptance_criteria: met # met | not_met
|
|
253
|
+
verification: passed # passed | failed | not_run
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Human-approved completion override:
|
|
257
|
+
```yaml
|
|
258
|
+
completion:
|
|
259
|
+
acceptance_criteria: not_met
|
|
260
|
+
verification: not_run
|
|
261
|
+
overridden_by: "@product-owner"
|
|
262
|
+
override_reason: "Human approved closing this ticket without meeting the usual done gates."
|
|
263
|
+
override_at: 2026-03-17T18:30:00Z
|
|
264
|
+
```
|
|
265
|
+
|
|
249
266
|
Limits:
|
|
250
267
|
```yaml
|
|
251
268
|
agent_limits:
|
|
@@ -271,6 +288,9 @@ custom:
|
|
|
271
288
|
|
|
272
289
|
Rules:
|
|
273
290
|
- `resolution` is only valid on terminal tickets (`done` or `canceled`)
|
|
291
|
+
- `completion` is required on tickets with `status: done`
|
|
292
|
+
- if `completion.acceptance_criteria != met` or `completion.verification != passed`, `completion.overridden_by`, `completion.override_reason`, and `completion.override_at` are required
|
|
293
|
+
- override fields are only valid when the usual completion gates were not fully satisfied
|
|
274
294
|
- `custom` is reserved for repo-local extensions not standardized by the spec
|
|
275
295
|
- other relationship views are computed by tooling and must not be persisted in `ticket.md`
|
|
276
296
|
|
|
@@ -321,6 +341,7 @@ Conditional fields:
|
|
|
321
341
|
|
|
322
342
|
Recommended fields:
|
|
323
343
|
- `changes`
|
|
344
|
+
- `completion`
|
|
324
345
|
- `verification`
|
|
325
346
|
- `tickets_created`
|
|
326
347
|
- `created_from`
|
|
@@ -376,9 +397,12 @@ Agents should:
|
|
|
376
397
|
2. Open the assigned ticket
|
|
377
398
|
3. Consult `.tickets/config.yml` for repo-local defaults and semantic overrides before interpreting planning terms or creating tickets
|
|
378
399
|
4. Validate before implementation
|
|
379
|
-
5.
|
|
380
|
-
6.
|
|
381
|
-
7.
|
|
400
|
+
5. Before setting a ticket to `done`, confirm the ticket's `## Acceptance Criteria` are met and its `## Verification` checks passed
|
|
401
|
+
6. If those completion gates are not satisfied, stop and ask a human whether to keep working or explicitly override the gates
|
|
402
|
+
7. When a human overrides incomplete completion gates, record that override in `ticket.md` and the status log via `npx @picoai/tickets status --status done --acceptance-criteria ... --verification-state ... --override-by ... --override-reason ...`
|
|
403
|
+
8. Respect `assignment.mode`, `agent_limits`, planning constraints, and active claims
|
|
404
|
+
9. Use `status`, `log`, `claim`, `list`, `plan`, and `graph` through the CLI
|
|
405
|
+
10. If splitting work, create child tickets with copied minimum context and log `created_from`
|
|
382
406
|
|
|
383
407
|
## Safety and hygiene
|
|
384
408
|
- Do not write secrets into tickets or logs
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Version: 1
|
|
4
4
|
- Version URL: `version/20260205-tickets-spec.md`
|
|
5
5
|
- Released: 2026-02-05
|
|
6
|
-
- Status:
|
|
6
|
+
- Status: superseded
|
|
7
7
|
|
|
8
8
|
## Definition (format only)
|
|
9
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.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Version: 2
|
|
4
4
|
- Version URL: `version/20260311-tickets-spec.md`
|
|
5
5
|
- Released: 2026-03-11
|
|
6
|
-
- Status:
|
|
6
|
+
- Status: superseded
|
|
7
7
|
|
|
8
8
|
## Definition (format only)
|
|
9
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.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Version: 3
|
|
4
4
|
- Version URL: `version/20260317-2-tickets-spec.md`
|
|
5
5
|
- Released: 2026-03-17
|
|
6
|
-
- Status:
|
|
6
|
+
- Status: superseded
|
|
7
7
|
|
|
8
8
|
## Definition (format and derived tooling contract)
|
|
9
9
|
This version defines the ticket, repo config, log, and derived planning-index contracts used by this repo. Workflow policy and narrative guidance live in `TICKETS.md`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Ticket Format Spec (Version 3)
|
|
2
|
+
|
|
3
|
+
- Version: 3
|
|
4
|
+
- Version URL: `version/20260317-3-tickets-spec.md`
|
|
5
|
+
- Released: 2026-03-17
|
|
6
|
+
- Status: superseded
|
|
7
|
+
|
|
8
|
+
## Definition (format and derived tooling contract)
|
|
9
|
+
This version defines the ticket, repo config, log, and derived planning-index contracts used by this repo. Workflow policy and narrative guidance live in `TICKETS.md`.
|
|
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
|
+
### Ticket front matter (optional)
|
|
20
|
+
- `priority`: `low|medium|high|critical`
|
|
21
|
+
- `labels`: list of strings
|
|
22
|
+
- `assignment`: mapping
|
|
23
|
+
- `dependencies`: list of ticket IDs
|
|
24
|
+
- `blocks`: list of ticket IDs
|
|
25
|
+
- `related`: list of ticket IDs
|
|
26
|
+
- `planning`: mapping
|
|
27
|
+
- `node_type`: `work|group|checkpoint`
|
|
28
|
+
- `group_ids`: list of ticket IDs
|
|
29
|
+
- `lane`: string or null
|
|
30
|
+
- `rank`: positive integer or null
|
|
31
|
+
- `horizon`: string or null
|
|
32
|
+
- `precedes`: list of ticket IDs
|
|
33
|
+
- `resolution`: `completed|merged|dropped|null`
|
|
34
|
+
- `completion`: mapping
|
|
35
|
+
- `acceptance_criteria`: `met|not_met`
|
|
36
|
+
- `verification`: `passed|failed|not_run`
|
|
37
|
+
- `overridden_by`: string or null
|
|
38
|
+
- `override_reason`: string or null
|
|
39
|
+
- `override_at`: ISO 8601 UTC timestamp or null
|
|
40
|
+
- `agent_limits`: mapping
|
|
41
|
+
- `verification`: mapping
|
|
42
|
+
- `custom`: mapping
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- `resolution` is only valid when `status` is terminal (`done` or `canceled`)
|
|
46
|
+
- `completion` is only valid when `status` is `done`
|
|
47
|
+
- if `completion.acceptance_criteria != met` or `completion.verification != passed`, `completion.overridden_by`, `completion.override_reason`, and `completion.override_at` are required
|
|
48
|
+
- override fields are only valid when the usual completion gates were not fully satisfied
|
|
49
|
+
- grouping is persisted only through `planning.group_ids`
|
|
50
|
+
- sequencing is persisted only through `planning.precedes`
|
|
51
|
+
- `planning.group_ids` may only reference `group` or `checkpoint` tickets
|
|
52
|
+
- `planning.precedes` must not contain cycles
|
|
53
|
+
- group/checkpoint containment must not contain cycles
|
|
54
|
+
- duplicate `rank` values are invalid within the same peer set (`node_type`, sorted `group_ids`, `lane`, `horizon`)
|
|
55
|
+
|
|
56
|
+
### Repo config (`.tickets/config.yml`)
|
|
57
|
+
- `workflow.mode`: `auto|doc_first|skill_first`
|
|
58
|
+
- `defaults.planning.node_type`: `work|group|checkpoint`
|
|
59
|
+
- `defaults.planning.lane`: string or null
|
|
60
|
+
- `defaults.planning.horizon`: string or null
|
|
61
|
+
- `defaults.claims.ttl_minutes`: positive integer
|
|
62
|
+
- `semantics.terms`: mapping from human-facing terms to generic planning primitives
|
|
63
|
+
- `views`: repo-local reporting preferences
|
|
64
|
+
|
|
65
|
+
Repo config may override defaults and human semantic mappings, but may not redefine CLI invariants, status values, or log schema.
|
|
66
|
+
|
|
67
|
+
### Log entry (required)
|
|
68
|
+
- `version`: format version (integer)
|
|
69
|
+
- `version_url`: path to this definition (repo-local, relative to `.tickets/spec/`)
|
|
70
|
+
- `ts`: ISO 8601 UTC timestamp
|
|
71
|
+
- `run_started`: ISO 8601 UTC timestamp
|
|
72
|
+
- `actor_type`: `human|agent`
|
|
73
|
+
- `actor_id`: string
|
|
74
|
+
- `summary`: short string
|
|
75
|
+
- `event_type`: `status|work|claim`
|
|
76
|
+
|
|
77
|
+
### Log entry (conditional)
|
|
78
|
+
- `context`: non-empty list of strings when `event_type: work` and the entry is machine-written
|
|
79
|
+
- `claim`: required mapping when `event_type: claim`
|
|
80
|
+
- `action`: `acquire|renew|release|override`
|
|
81
|
+
- `claim_id`: UUIDv7 string
|
|
82
|
+
- `holder_id`: string
|
|
83
|
+
- `holder_type`: `human|agent`
|
|
84
|
+
- `ttl_minutes`: positive integer for non-release events
|
|
85
|
+
- `expires_at`: ISO 8601 UTC timestamp for non-release events
|
|
86
|
+
- `reason`: optional string
|
|
87
|
+
- `supersedes_claim_id`: optional UUIDv7 string or null
|
|
88
|
+
|
|
89
|
+
### Log entry (optional)
|
|
90
|
+
- `completion`: mapping on `status` events when a done decision was recorded
|
|
91
|
+
- `acceptance_criteria`: `met|not_met`
|
|
92
|
+
- `verification`: `passed|failed|not_run`
|
|
93
|
+
- `overridden_by`: string or null
|
|
94
|
+
- `override_reason`: string or null
|
|
95
|
+
- `override_at`: ISO 8601 UTC timestamp or null
|
|
96
|
+
|
|
97
|
+
### Derived planning index (`/.tickets/derived/planning-index.json`)
|
|
98
|
+
- The index is derived cache state, not source of truth.
|
|
99
|
+
- The file is disposable and may be rebuilt at any time by the CLI.
|
|
100
|
+
- Required metadata:
|
|
101
|
+
- `index_format_id`: fixed UUIDv7 identifying the derived index format
|
|
102
|
+
- `index_format_label`: readable label for the index format revision
|
|
103
|
+
- `tool.format_version`
|
|
104
|
+
- `tool.format_version_url`
|
|
105
|
+
- `source_state`
|
|
106
|
+
- The CLI must rebuild the index when source files or embedded format/tool metadata no longer match.
|
|
107
|
+
|
|
108
|
+
### Reporting semantics
|
|
109
|
+
- `list` is the broad queue/report view.
|
|
110
|
+
- `plan` is the operational state view for ready, active, blocked, and group rollups.
|
|
111
|
+
- `graph` is the structural relationship view.
|
|
112
|
+
- Repo-specific planning language remains authoritative only in `.tickets/config.yml`.
|
|
113
|
+
|
|
114
|
+
### Extensions
|
|
115
|
+
- Extensions are repo-local and must live under the `custom` key.
|
|
116
|
+
- Tools should ignore unknown keys under `custom`.
|
|
117
|
+
|
|
118
|
+
## Diff from previous version
|
|
119
|
+
- Added standardized `completion` metadata for recording whether acceptance criteria and verification passed before a ticket was moved to `done`.
|
|
120
|
+
- Added explicit human-override fields for done tickets that were closed without fully satisfying the usual completion gates.
|
|
121
|
+
- Allowed status log entries to mirror recorded `completion` decisions for auditability.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Ticket Format Spec (Version 3)
|
|
2
|
+
|
|
3
|
+
- Version: 3
|
|
4
|
+
- Version URL: `version/20260317-4-tickets-spec.md`
|
|
5
|
+
- Released: 2026-03-17
|
|
6
|
+
- Status: current
|
|
7
|
+
|
|
8
|
+
## Definition (format and derived tooling contract)
|
|
9
|
+
This version defines the ticket, repo config, log, and derived planning-index contracts used by this repo. Workflow policy and narrative guidance live in `TICKETS.md`.
|
|
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
|
+
### Ticket front matter (optional)
|
|
20
|
+
- `priority`: `low|medium|high|critical`
|
|
21
|
+
- `labels`: list of strings
|
|
22
|
+
- `assignment`: mapping
|
|
23
|
+
- `dependencies`: list of ticket IDs
|
|
24
|
+
- `blocks`: list of ticket IDs
|
|
25
|
+
- `related`: list of ticket IDs
|
|
26
|
+
- `planning`: mapping
|
|
27
|
+
- `node_type`: `work|group|checkpoint`
|
|
28
|
+
- `group_ids`: list of ticket IDs
|
|
29
|
+
- `lane`: string or null
|
|
30
|
+
- `rank`: positive integer or null
|
|
31
|
+
- `horizon`: string or null
|
|
32
|
+
- `precedes`: list of ticket IDs
|
|
33
|
+
- `resolution`: `completed|merged|dropped|null`
|
|
34
|
+
- `completion`: mapping
|
|
35
|
+
- `acceptance_criteria`: `met|not_met`
|
|
36
|
+
- `verification`: `passed|failed|not_run`
|
|
37
|
+
- `overridden_by`: string or null
|
|
38
|
+
- `override_reason`: string or null
|
|
39
|
+
- `override_at`: ISO 8601 UTC timestamp or null
|
|
40
|
+
- `agent_limits`: mapping
|
|
41
|
+
- `verification`: mapping
|
|
42
|
+
- `custom`: mapping
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- `resolution` is only valid when `status` is terminal (`done` or `canceled`)
|
|
46
|
+
- `completion` is required when `status` is `done`
|
|
47
|
+
- if `completion.acceptance_criteria != met` or `completion.verification != passed`, `completion.overridden_by`, `completion.override_reason`, and `completion.override_at` are required
|
|
48
|
+
- override fields are only valid when the usual completion gates were not fully satisfied
|
|
49
|
+
- grouping is persisted only through `planning.group_ids`
|
|
50
|
+
- sequencing is persisted only through `planning.precedes`
|
|
51
|
+
- `planning.group_ids` may only reference `group` or `checkpoint` tickets
|
|
52
|
+
- `planning.precedes` must not contain cycles
|
|
53
|
+
- group/checkpoint containment must not contain cycles
|
|
54
|
+
- duplicate `rank` values are invalid within the same peer set (`node_type`, sorted `group_ids`, `lane`, `horizon`)
|
|
55
|
+
|
|
56
|
+
### Repo config (`.tickets/config.yml`)
|
|
57
|
+
- `workflow.mode`: `auto|doc_first|skill_first`
|
|
58
|
+
- `defaults.planning.node_type`: `work|group|checkpoint`
|
|
59
|
+
- `defaults.planning.lane`: string or null
|
|
60
|
+
- `defaults.planning.horizon`: string or null
|
|
61
|
+
- `defaults.claims.ttl_minutes`: positive integer
|
|
62
|
+
- `semantics.terms`: mapping from human-facing terms to generic planning primitives
|
|
63
|
+
- `views`: repo-local reporting preferences
|
|
64
|
+
|
|
65
|
+
Repo config may override defaults and human semantic mappings, but may not redefine CLI invariants, status values, or log schema.
|
|
66
|
+
|
|
67
|
+
### Log entry (required)
|
|
68
|
+
- `version`: format version (integer)
|
|
69
|
+
- `version_url`: path to this definition (repo-local, relative to `.tickets/spec/`)
|
|
70
|
+
- `ts`: ISO 8601 UTC timestamp
|
|
71
|
+
- `run_started`: ISO 8601 UTC timestamp
|
|
72
|
+
- `actor_type`: `human|agent`
|
|
73
|
+
- `actor_id`: string
|
|
74
|
+
- `summary`: short string
|
|
75
|
+
- `event_type`: `status|work|claim`
|
|
76
|
+
|
|
77
|
+
### Log entry (conditional)
|
|
78
|
+
- `context`: non-empty list of strings when `event_type: work` and the entry is machine-written
|
|
79
|
+
- `claim`: required mapping when `event_type: claim`
|
|
80
|
+
- `action`: `acquire|renew|release|override`
|
|
81
|
+
- `claim_id`: UUIDv7 string
|
|
82
|
+
- `holder_id`: string
|
|
83
|
+
- `holder_type`: `human|agent`
|
|
84
|
+
- `ttl_minutes`: positive integer for non-release events
|
|
85
|
+
- `expires_at`: ISO 8601 UTC timestamp for non-release events
|
|
86
|
+
- `reason`: optional string
|
|
87
|
+
- `supersedes_claim_id`: optional UUIDv7 string or null
|
|
88
|
+
|
|
89
|
+
### Log entry (optional)
|
|
90
|
+
- `completion`: mapping on `status` events when a done decision was recorded
|
|
91
|
+
- `acceptance_criteria`: `met|not_met`
|
|
92
|
+
- `verification`: `passed|failed|not_run`
|
|
93
|
+
- `overridden_by`: string or null
|
|
94
|
+
- `override_reason`: string or null
|
|
95
|
+
- `override_at`: ISO 8601 UTC timestamp or null
|
|
96
|
+
|
|
97
|
+
### Derived planning index (`/.tickets/derived/planning-index.json`)
|
|
98
|
+
- The index is derived cache state, not source of truth.
|
|
99
|
+
- The file is disposable and may be rebuilt at any time by the CLI.
|
|
100
|
+
- Required metadata:
|
|
101
|
+
- `index_format_id`: fixed UUIDv7 identifying the derived index format
|
|
102
|
+
- `index_format_label`: readable label for the index format revision
|
|
103
|
+
- `tool.format_version`
|
|
104
|
+
- `tool.format_version_url`
|
|
105
|
+
- `source_state`
|
|
106
|
+
- The CLI must rebuild the index when source files or embedded format/tool metadata no longer match.
|
|
107
|
+
|
|
108
|
+
### Reporting semantics
|
|
109
|
+
- `list` is the broad queue/report view.
|
|
110
|
+
- `plan` is the operational state view for ready, active, blocked, and group rollups.
|
|
111
|
+
- `graph` is the structural relationship view.
|
|
112
|
+
- Repo-specific planning language remains authoritative only in `.tickets/config.yml`.
|
|
113
|
+
|
|
114
|
+
### Extensions
|
|
115
|
+
- Extensions are repo-local and must live under the `custom` key.
|
|
116
|
+
- Tools should ignore unknown keys under `custom`.
|
|
117
|
+
|
|
118
|
+
## Diff from previous version
|
|
119
|
+
- Tightened the `completion` contract so every `done` ticket must persist completion metadata.
|
|
120
|
+
- Kept human-approved completion overrides as the supported path for closing tickets when usual done gates are not fully satisfied.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Version: 3
|
|
4
4
|
- Version URL: `version/20260317-tickets-spec.md`
|
|
5
5
|
- Released: 2026-03-17
|
|
6
|
-
- Status:
|
|
6
|
+
- Status: superseded
|
|
7
7
|
|
|
8
8
|
## Definition (format only)
|
|
9
9
|
This version defines the ticket, repo config, and log formats used by this repo. Workflow policy and narrative guidance live in `TICKETS.md`.
|
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ The system is designed for teams that want ticket state to live in the repo, sta
|
|
|
24
24
|
## Spec version
|
|
25
25
|
|
|
26
26
|
- `version`: 3
|
|
27
|
-
- `version_url`: `version/20260317-
|
|
28
|
-
- Local file in package assets: `.tickets/spec/version/20260317-
|
|
27
|
+
- `version_url`: `version/20260317-4-tickets-spec.md`
|
|
28
|
+
- Local file in package assets: `.tickets/spec/version/20260317-4-tickets-spec.md`
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -79,6 +79,8 @@ How to think about them:
|
|
|
79
79
|
- `precedes`: sequence edges without turning everything into a hard dependency
|
|
80
80
|
- `resolution`: terminal outcome when work was completed, merged away, or dropped
|
|
81
81
|
|
|
82
|
+
Completion is tracked separately from terminal outcome, and it is required on every `done` ticket. A ticket should only be treated as done when its acceptance criteria are met and its verification checks pass. If a human explicitly wants to close a ticket without those gates passing, record that override in `completion` so the exception is visible in both `ticket.md` and the status log.
|
|
83
|
+
|
|
82
84
|
Default semantic mapping:
|
|
83
85
|
- `feature` -> `planning.node_type=group`
|
|
84
86
|
- `phase` -> `planning.lane`
|
package/package.json
CHANGED
package/release-history.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import yaml from "yaml";
|
|
|
8
8
|
import {
|
|
9
9
|
ASSIGNMENT_MODE_VALUES,
|
|
10
10
|
BASE_DIR,
|
|
11
|
+
COMPLETION_ACCEPTANCE_VALUES,
|
|
12
|
+
COMPLETION_VERIFICATION_VALUES,
|
|
11
13
|
DEFAULT_CLAIM_TTL_MINUTES,
|
|
12
14
|
FORMAT_VERSION,
|
|
13
15
|
FORMAT_VERSION_URL,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
loadTicket,
|
|
35
37
|
newUuidv7,
|
|
36
38
|
nowUtc,
|
|
39
|
+
parseIso,
|
|
37
40
|
readTemplate,
|
|
38
41
|
repoRoot,
|
|
39
42
|
resolveTicketPath,
|
|
@@ -96,6 +99,102 @@ function normalizeContextItems(values) {
|
|
|
96
99
|
return values.map((value) => String(value).trim()).filter(Boolean);
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
function hasCompletionOptions(options) {
|
|
103
|
+
return [
|
|
104
|
+
options.acceptanceCriteria,
|
|
105
|
+
options.verificationState,
|
|
106
|
+
options.overrideBy,
|
|
107
|
+
options.overrideReason,
|
|
108
|
+
options.overrideAt,
|
|
109
|
+
].some((value) => value !== undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function trimOptionalString(value) {
|
|
113
|
+
if (value === undefined || value === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const normalized = String(value).trim();
|
|
117
|
+
return normalized ? normalized : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildCompletionRecord(existingCompletion, options) {
|
|
121
|
+
const explicitCompletion = hasCompletionOptions(options);
|
|
122
|
+
if (!explicitCompletion) {
|
|
123
|
+
if (options.status !== "done") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (!existingCompletion || typeof existingCompletion !== "object" || Array.isArray(existingCompletion)) {
|
|
127
|
+
throw new Error("Completion data is required when --status done");
|
|
128
|
+
}
|
|
129
|
+
return existingCompletion;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.status !== "done") {
|
|
133
|
+
throw new Error("Completion data is only valid when --status done");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const completion =
|
|
137
|
+
existingCompletion && typeof existingCompletion === "object" && !Array.isArray(existingCompletion)
|
|
138
|
+
? { ...existingCompletion }
|
|
139
|
+
: {};
|
|
140
|
+
|
|
141
|
+
if (options.acceptanceCriteria !== undefined) {
|
|
142
|
+
completion.acceptance_criteria = options.acceptanceCriteria;
|
|
143
|
+
}
|
|
144
|
+
if (options.verificationState !== undefined) {
|
|
145
|
+
completion.verification = options.verificationState;
|
|
146
|
+
}
|
|
147
|
+
if (options.overrideBy !== undefined) {
|
|
148
|
+
completion.overridden_by = trimOptionalString(options.overrideBy);
|
|
149
|
+
}
|
|
150
|
+
if (options.overrideReason !== undefined) {
|
|
151
|
+
completion.override_reason = trimOptionalString(options.overrideReason);
|
|
152
|
+
}
|
|
153
|
+
if (options.overrideAt !== undefined) {
|
|
154
|
+
completion.override_at = options.overrideAt;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!COMPLETION_ACCEPTANCE_VALUES.includes(completion.acceptance_criteria)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Completion acceptance criteria must be one of: ${COMPLETION_ACCEPTANCE_VALUES.join(", ")}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!COMPLETION_VERIFICATION_VALUES.includes(completion.verification)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Completion verification state must be one of: ${COMPLETION_VERIFICATION_VALUES.join(", ")}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const overrideRequired =
|
|
169
|
+
completion.acceptance_criteria !== "met" || completion.verification !== "passed";
|
|
170
|
+
if (overrideRequired) {
|
|
171
|
+
if (!trimOptionalString(completion.overridden_by)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Human override approval is required when acceptance criteria are not met or verification did not pass",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (!trimOptionalString(completion.override_reason)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"Override reason is required when acceptance criteria are not met or verification did not pass",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (completion.override_at === undefined || completion.override_at === null) {
|
|
182
|
+
completion.override_at = iso8601(nowUtc());
|
|
183
|
+
}
|
|
184
|
+
if (!parseIso(completion.override_at)) {
|
|
185
|
+
throw new Error("Override timestamp must be ISO8601 UTC");
|
|
186
|
+
}
|
|
187
|
+
completion.overridden_by = trimOptionalString(completion.overridden_by);
|
|
188
|
+
completion.override_reason = trimOptionalString(completion.override_reason);
|
|
189
|
+
return completion;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
delete completion.overridden_by;
|
|
193
|
+
delete completion.override_reason;
|
|
194
|
+
delete completion.override_at;
|
|
195
|
+
return completion;
|
|
196
|
+
}
|
|
197
|
+
|
|
99
198
|
function groupIdSignature(groupIds = []) {
|
|
100
199
|
return [...groupIds].sort((a, b) => a.localeCompare(b)).join(",");
|
|
101
200
|
}
|
|
@@ -1052,18 +1151,24 @@ async function cmdInit(options) {
|
|
|
1052
1151
|
const versionDir = path.join(repoBaseDir, "version");
|
|
1053
1152
|
ensureDir(versionDir);
|
|
1054
1153
|
|
|
1055
|
-
const currentSpecPath = path.join(versionDir, "20260317-
|
|
1056
|
-
writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "20260317-
|
|
1154
|
+
const currentSpecPath = path.join(versionDir, "20260317-4-tickets-spec.md");
|
|
1155
|
+
writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "20260317-4-tickets-spec.md"), apply);
|
|
1057
1156
|
|
|
1058
|
-
const previousCurrentSpecPath = path.join(versionDir, "
|
|
1157
|
+
const previousCurrentSpecPath = path.join(versionDir, "20260317-3-tickets-spec.md");
|
|
1059
1158
|
writeTemplateFile(
|
|
1060
1159
|
previousCurrentSpecPath,
|
|
1061
|
-
path.join(".tickets", "spec", "version", "
|
|
1160
|
+
path.join(".tickets", "spec", "version", "20260317-3-tickets-spec.md"),
|
|
1062
1161
|
apply,
|
|
1063
1162
|
);
|
|
1064
1163
|
|
|
1065
|
-
const previousSpecPath = path.join(versionDir, "
|
|
1066
|
-
writeTemplateFile(previousSpecPath, path.join(".tickets", "spec", "version", "
|
|
1164
|
+
const previousSpecPath = path.join(versionDir, "20260317-2-tickets-spec.md");
|
|
1165
|
+
writeTemplateFile(previousSpecPath, path.join(".tickets", "spec", "version", "20260317-2-tickets-spec.md"), apply);
|
|
1166
|
+
|
|
1167
|
+
const olderSpecPath = path.join(versionDir, "20260311-tickets-spec.md");
|
|
1168
|
+
writeTemplateFile(olderSpecPath, path.join(".tickets", "spec", "version", "20260311-tickets-spec.md"), apply);
|
|
1169
|
+
|
|
1170
|
+
const legacySpecPath = path.join(versionDir, "20260205-tickets-spec.md");
|
|
1171
|
+
writeTemplateFile(legacySpecPath, path.join(".tickets", "spec", "version", "20260205-tickets-spec.md"), apply);
|
|
1067
1172
|
|
|
1068
1173
|
const proposedPath = path.join(versionDir, "PROPOSED-tickets-spec.md");
|
|
1069
1174
|
writeTemplateFile(proposedPath, path.join(".tickets", "spec", "version", "PROPOSED-tickets-spec.md"), apply);
|
|
@@ -1115,6 +1220,8 @@ async function cmdNew(options) {
|
|
|
1115
1220
|
planning.rank = inferNextRank(snapshot, planning);
|
|
1116
1221
|
}
|
|
1117
1222
|
|
|
1223
|
+
const completion = buildCompletionRecord(null, options);
|
|
1224
|
+
|
|
1118
1225
|
const frontMatter = {
|
|
1119
1226
|
id: ticketId,
|
|
1120
1227
|
version: FORMAT_VERSION,
|
|
@@ -1168,6 +1275,9 @@ async function cmdNew(options) {
|
|
|
1168
1275
|
frontMatter.verification = { commands: options.verificationCommands };
|
|
1169
1276
|
}
|
|
1170
1277
|
frontMatter.planning = planning;
|
|
1278
|
+
if (completion) {
|
|
1279
|
+
frontMatter.completion = completion;
|
|
1280
|
+
}
|
|
1171
1281
|
if (options.resolution) {
|
|
1172
1282
|
frontMatter.resolution = options.resolution;
|
|
1173
1283
|
}
|
|
@@ -1259,12 +1369,24 @@ async function cmdStatus(options) {
|
|
|
1259
1369
|
const actorId = resolveActorId(options.actorId);
|
|
1260
1370
|
const actorType = resolveActorType(options.actorType, actorId);
|
|
1261
1371
|
const context = normalizeContextItems(options.context);
|
|
1372
|
+
const completion = buildCompletionRecord(frontMatter.completion, options);
|
|
1262
1373
|
|
|
1263
1374
|
frontMatter.status = options.status;
|
|
1375
|
+
if (options.status === "done" && completion) {
|
|
1376
|
+
frontMatter.completion = completion;
|
|
1377
|
+
} else {
|
|
1378
|
+
delete frontMatter.completion;
|
|
1379
|
+
}
|
|
1380
|
+
if (!["done", "canceled"].includes(options.status)) {
|
|
1381
|
+
delete frontMatter.resolution;
|
|
1382
|
+
}
|
|
1264
1383
|
writeTicket(ticketPath, frontMatter, body);
|
|
1265
1384
|
|
|
1266
1385
|
const runId = options.runId || newUuidv7();
|
|
1267
1386
|
const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
|
|
1387
|
+
const hasCompletionOverride =
|
|
1388
|
+
completion &&
|
|
1389
|
+
(completion.acceptance_criteria !== "met" || completion.verification !== "passed");
|
|
1268
1390
|
const entry = {
|
|
1269
1391
|
version: FORMAT_VERSION,
|
|
1270
1392
|
version_url: FORMAT_VERSION_URL,
|
|
@@ -1274,14 +1396,17 @@ async function cmdStatus(options) {
|
|
|
1274
1396
|
actor_id: actorId,
|
|
1275
1397
|
summary:
|
|
1276
1398
|
previousStatus === options.status
|
|
1277
|
-
? `Status reaffirmed as ${options.status}`
|
|
1278
|
-
: `Status changed from ${previousStatus ?? "unknown"} to ${options.status}`,
|
|
1399
|
+
? `Status reaffirmed as ${options.status}${hasCompletionOverride ? " with human override" : ""}`
|
|
1400
|
+
: `Status changed from ${previousStatus ?? "unknown"} to ${options.status}${hasCompletionOverride ? " with human override" : ""}`,
|
|
1279
1401
|
event_type: "status",
|
|
1280
1402
|
written_by: "tickets",
|
|
1281
1403
|
};
|
|
1282
1404
|
if (context.length > 0) {
|
|
1283
1405
|
entry.context = context;
|
|
1284
1406
|
}
|
|
1407
|
+
if (options.status === "done" && completion) {
|
|
1408
|
+
entry.completion = completion;
|
|
1409
|
+
}
|
|
1285
1410
|
|
|
1286
1411
|
const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
|
|
1287
1412
|
appendJsonl(logPath, entry);
|
|
@@ -1695,10 +1820,31 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1695
1820
|
.option("--horizon <horizon>")
|
|
1696
1821
|
.option("--precedes <ticketId>", "Sequence successor ticket id", collectOption, [])
|
|
1697
1822
|
.option("--resolution <resolution>")
|
|
1823
|
+
.option("--acceptance-criteria <state>", "Completion acceptance state: met | not_met")
|
|
1824
|
+
.option("--verification-state <state>", "Completion verification state: passed | failed | not_run")
|
|
1825
|
+
.option("--override-by <actorId>", "Human who approved bypassing completion gates")
|
|
1826
|
+
.option("--override-reason <reason>", "Why the ticket was closed without passing all completion gates")
|
|
1827
|
+
.option("--override-at <timestamp>", "When the human override was approved (ISO8601 UTC)")
|
|
1698
1828
|
.action(async (options) => {
|
|
1699
1829
|
if (!STATUS_VALUES.includes(options.status)) {
|
|
1700
1830
|
throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
|
|
1701
1831
|
}
|
|
1832
|
+
if (
|
|
1833
|
+
options.acceptanceCriteria &&
|
|
1834
|
+
!COMPLETION_ACCEPTANCE_VALUES.includes(options.acceptanceCriteria)
|
|
1835
|
+
) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`Invalid --acceptance-criteria. Use one of: ${COMPLETION_ACCEPTANCE_VALUES.join(", ")}`,
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
if (
|
|
1841
|
+
options.verificationState &&
|
|
1842
|
+
!COMPLETION_VERIFICATION_VALUES.includes(options.verificationState)
|
|
1843
|
+
) {
|
|
1844
|
+
throw new Error(
|
|
1845
|
+
`Invalid --verification-state. Use one of: ${COMPLETION_VERIFICATION_VALUES.join(", ")}`,
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1702
1848
|
if (options.priority && !PRIORITY_VALUES.includes(options.priority)) {
|
|
1703
1849
|
throw new Error(`Invalid --priority. Use one of: ${PRIORITY_VALUES.join(", ")}`);
|
|
1704
1850
|
}
|
|
@@ -1749,6 +1895,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1749
1895
|
horizon: options.horizon,
|
|
1750
1896
|
precedes: options.precedes,
|
|
1751
1897
|
resolution: options.resolution,
|
|
1898
|
+
acceptanceCriteria: options.acceptanceCriteria,
|
|
1899
|
+
verificationState: options.verificationState,
|
|
1900
|
+
overrideBy: options.overrideBy,
|
|
1901
|
+
overrideReason: options.overrideReason,
|
|
1902
|
+
overrideAt: options.overrideAt,
|
|
1752
1903
|
});
|
|
1753
1904
|
});
|
|
1754
1905
|
|
|
@@ -1773,6 +1924,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1773
1924
|
.description("Update ticket status")
|
|
1774
1925
|
.requiredOption("--ticket <ticket>")
|
|
1775
1926
|
.requiredOption("--status <status>")
|
|
1927
|
+
.option("--acceptance-criteria <state>", "Completion acceptance state: met | not_met")
|
|
1928
|
+
.option("--verification-state <state>", "Completion verification state: passed | failed | not_run")
|
|
1929
|
+
.option("--override-by <actorId>", "Human who approved bypassing completion gates")
|
|
1930
|
+
.option("--override-reason <reason>", "Why the ticket was closed without passing all completion gates")
|
|
1931
|
+
.option("--override-at <timestamp>", "When the human override was approved (ISO8601 UTC)")
|
|
1776
1932
|
.option("--actor-type <actorType>")
|
|
1777
1933
|
.option("--actor-id <actorId>")
|
|
1778
1934
|
.option("--context <items...>")
|
|
@@ -1782,12 +1938,33 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1782
1938
|
if (!STATUS_VALUES.includes(options.status)) {
|
|
1783
1939
|
throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
|
|
1784
1940
|
}
|
|
1941
|
+
if (
|
|
1942
|
+
options.acceptanceCriteria &&
|
|
1943
|
+
!COMPLETION_ACCEPTANCE_VALUES.includes(options.acceptanceCriteria)
|
|
1944
|
+
) {
|
|
1945
|
+
throw new Error(
|
|
1946
|
+
`Invalid --acceptance-criteria. Use one of: ${COMPLETION_ACCEPTANCE_VALUES.join(", ")}`,
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
if (
|
|
1950
|
+
options.verificationState &&
|
|
1951
|
+
!COMPLETION_VERIFICATION_VALUES.includes(options.verificationState)
|
|
1952
|
+
) {
|
|
1953
|
+
throw new Error(
|
|
1954
|
+
`Invalid --verification-state. Use one of: ${COMPLETION_VERIFICATION_VALUES.join(", ")}`,
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1785
1957
|
if (options.actorType && !isValidActorType(options.actorType)) {
|
|
1786
1958
|
throw new Error("Invalid --actor-type. Use one of: human, agent");
|
|
1787
1959
|
}
|
|
1788
1960
|
process.exitCode = await cmdStatus({
|
|
1789
1961
|
ticket: options.ticket,
|
|
1790
1962
|
status: options.status,
|
|
1963
|
+
acceptanceCriteria: options.acceptanceCriteria,
|
|
1964
|
+
verificationState: options.verificationState,
|
|
1965
|
+
overrideBy: options.overrideBy,
|
|
1966
|
+
overrideReason: options.overrideReason,
|
|
1967
|
+
overrideAt: options.overrideAt,
|
|
1791
1968
|
actorType: options.actorType,
|
|
1792
1969
|
actorId: options.actorId,
|
|
1793
1970
|
context: options.context,
|
package/src/lib/constants.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export const BASE_DIR = ".tickets/spec";
|
|
2
2
|
export const FORMAT_VERSION = 3;
|
|
3
|
-
export const FORMAT_VERSION_URL = "version/20260317-
|
|
3
|
+
export const FORMAT_VERSION_URL = "version/20260317-4-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"];
|
|
7
7
|
export const ASSIGNMENT_MODE_VALUES = ["human_only", "agent_only", "mixed"];
|
|
8
8
|
export const PLANNING_NODE_TYPES = ["work", "group", "checkpoint"];
|
|
9
9
|
export const RESOLUTION_VALUES = ["completed", "merged", "dropped"];
|
|
10
|
+
export const COMPLETION_ACCEPTANCE_VALUES = ["met", "not_met"];
|
|
11
|
+
export const COMPLETION_VERIFICATION_VALUES = ["passed", "failed", "not_run"];
|
|
10
12
|
export const CLAIM_ACTION_VALUES = ["acquire", "renew", "release", "override"];
|
|
11
13
|
export const WORKFLOW_MODE_VALUES = ["auto", "doc_first", "skill_first"];
|
|
12
14
|
export const GRAPH_VIEW_VALUES = ["dependency", "sequence", "portfolio", "all"];
|
package/src/lib/projections.js
CHANGED
|
@@ -32,6 +32,10 @@ export function renderRepoSkill(profile = loadDefaultProfile()) {
|
|
|
32
32
|
"- Read `TICKETS.md` for the full repo contract when context is missing.",
|
|
33
33
|
"- Consult `.tickets/config.yml` for repo-local defaults and semantic overrides before interpreting planning terminology or creating new tickets.",
|
|
34
34
|
"- Validate assigned tickets before implementation with `npx @picoai/tickets validate`.",
|
|
35
|
+
"- Before setting a ticket to `done`, confirm the ticket's `## Acceptance Criteria` are met and its `## Verification` checks passed.",
|
|
36
|
+
"- If those completion gates are not satisfied, stop and ask a human whether to keep working or explicitly override the gates. Only move the ticket to `done` after that human decision.",
|
|
37
|
+
"- Record `completion` metadata every time a ticket is moved to `done`.",
|
|
38
|
+
"- When a human overrides incomplete completion gates, record the exception through `npx @picoai/tickets status --status done --acceptance-criteria ... --verification-state ... --override-by ... --override-reason ...` so `ticket.md` and the status log both reflect it.",
|
|
35
39
|
"- Use `npx @picoai/tickets status`, `log`, `claim`, `plan`, and `graph` instead of editing derived state manually.",
|
|
36
40
|
"- When humans use terms like feature, phase, milestone, roadmap, or repo-specific equivalents, translate them through `.tickets/config.yml` and then call the generic CLI fields.",
|
|
37
41
|
"- Respect repo overrides in `.tickets/config.yml` and any narrative guidance in `TICKETS.override.md` if present.",
|
package/src/lib/validation.js
CHANGED
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
ASSIGNMENT_MODE_VALUES,
|
|
6
6
|
CLAIM_ACTION_VALUES,
|
|
7
|
+
COMPLETION_ACCEPTANCE_VALUES,
|
|
8
|
+
COMPLETION_VERIFICATION_VALUES,
|
|
7
9
|
PLANNING_NODE_TYPES,
|
|
8
10
|
PRIORITY_VALUES,
|
|
9
11
|
RESOLUTION_VALUES,
|
|
@@ -298,6 +300,124 @@ export function validateTicket(ticketPath, allFields = false) {
|
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
|
|
303
|
+
if ("completion" in frontMatter) {
|
|
304
|
+
const completion = frontMatter.completion;
|
|
305
|
+
if (!completion || typeof completion !== "object" || Array.isArray(completion)) {
|
|
306
|
+
issues.push({
|
|
307
|
+
severity: "error",
|
|
308
|
+
code: "COMPLETION_INVALID",
|
|
309
|
+
message: "completion must be mapping",
|
|
310
|
+
ticket_path: ticketPath,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
const acceptance = completion.acceptance_criteria;
|
|
314
|
+
const verification = completion.verification;
|
|
315
|
+
const overriddenBy = completion.overridden_by;
|
|
316
|
+
const overrideReason = completion.override_reason;
|
|
317
|
+
const overrideAt = completion.override_at;
|
|
318
|
+
|
|
319
|
+
if (!COMPLETION_ACCEPTANCE_VALUES.includes(acceptance)) {
|
|
320
|
+
issues.push({
|
|
321
|
+
severity: "error",
|
|
322
|
+
code: "COMPLETION_ACCEPTANCE_INVALID",
|
|
323
|
+
message: `completion.acceptance_criteria must be one of ${COMPLETION_ACCEPTANCE_VALUES.join("|")}`,
|
|
324
|
+
ticket_path: ticketPath,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!COMPLETION_VERIFICATION_VALUES.includes(verification)) {
|
|
329
|
+
issues.push({
|
|
330
|
+
severity: "error",
|
|
331
|
+
code: "COMPLETION_VERIFICATION_INVALID",
|
|
332
|
+
message: `completion.verification must be one of ${COMPLETION_VERIFICATION_VALUES.join("|")}`,
|
|
333
|
+
ticket_path: ticketPath,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (frontMatter.status !== "done") {
|
|
338
|
+
issues.push({
|
|
339
|
+
severity: "error",
|
|
340
|
+
code: "COMPLETION_STATUS_INVALID",
|
|
341
|
+
message: "completion requires status done",
|
|
342
|
+
ticket_path: ticketPath,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const overrideRequired = acceptance !== "met" || verification !== "passed";
|
|
347
|
+
const hasOverrideBy = typeof overriddenBy === "string" && overriddenBy.trim();
|
|
348
|
+
const hasOverrideReason = typeof overrideReason === "string" && overrideReason.trim();
|
|
349
|
+
const hasOverrideAt = overrideAt !== undefined && overrideAt !== null;
|
|
350
|
+
|
|
351
|
+
if (overriddenBy !== undefined && overriddenBy !== null && typeof overriddenBy !== "string") {
|
|
352
|
+
issues.push({
|
|
353
|
+
severity: "error",
|
|
354
|
+
code: "COMPLETION_OVERRIDE_BY_INVALID",
|
|
355
|
+
message: "completion.overridden_by must be string or null",
|
|
356
|
+
ticket_path: ticketPath,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (overrideReason !== undefined && overrideReason !== null && typeof overrideReason !== "string") {
|
|
361
|
+
issues.push({
|
|
362
|
+
severity: "error",
|
|
363
|
+
code: "COMPLETION_OVERRIDE_REASON_INVALID",
|
|
364
|
+
message: "completion.override_reason must be string or null",
|
|
365
|
+
ticket_path: ticketPath,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (hasOverrideAt && !parseIso(overrideAt)) {
|
|
370
|
+
issues.push({
|
|
371
|
+
severity: "error",
|
|
372
|
+
code: "COMPLETION_OVERRIDE_AT_INVALID",
|
|
373
|
+
message: "completion.override_at must be ISO8601 UTC or null",
|
|
374
|
+
ticket_path: ticketPath,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (overrideRequired) {
|
|
379
|
+
if (!hasOverrideBy) {
|
|
380
|
+
issues.push({
|
|
381
|
+
severity: "error",
|
|
382
|
+
code: "COMPLETION_OVERRIDE_BY_MISSING",
|
|
383
|
+
message: "completion.overridden_by required when completion gates are not fully satisfied",
|
|
384
|
+
ticket_path: ticketPath,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (!hasOverrideReason) {
|
|
388
|
+
issues.push({
|
|
389
|
+
severity: "error",
|
|
390
|
+
code: "COMPLETION_OVERRIDE_REASON_MISSING",
|
|
391
|
+
message: "completion.override_reason required when completion gates are not fully satisfied",
|
|
392
|
+
ticket_path: ticketPath,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (!hasOverrideAt) {
|
|
396
|
+
issues.push({
|
|
397
|
+
severity: "error",
|
|
398
|
+
code: "COMPLETION_OVERRIDE_AT_MISSING",
|
|
399
|
+
message: "completion.override_at required when completion gates are not fully satisfied",
|
|
400
|
+
ticket_path: ticketPath,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} else if (hasOverrideBy || hasOverrideReason || hasOverrideAt) {
|
|
404
|
+
issues.push({
|
|
405
|
+
severity: "error",
|
|
406
|
+
code: "COMPLETION_OVERRIDE_UNEXPECTED",
|
|
407
|
+
message: "completion override fields are only valid when acceptance criteria or verification are not fully satisfied",
|
|
408
|
+
ticket_path: ticketPath,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} else if (frontMatter.status === "done") {
|
|
413
|
+
issues.push({
|
|
414
|
+
severity: "error",
|
|
415
|
+
code: "COMPLETION_MISSING",
|
|
416
|
+
message: "completion required when status is done",
|
|
417
|
+
ticket_path: ticketPath,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
301
421
|
if ("agent_limits" in frontMatter) {
|
|
302
422
|
if (!frontMatter.agent_limits || typeof frontMatter.agent_limits !== "object" || Array.isArray(frontMatter.agent_limits)) {
|
|
303
423
|
issues.push({
|
|
@@ -910,6 +1030,122 @@ export function validateRunLog(logPath, machineStrictDefault) {
|
|
|
910
1030
|
log: loc,
|
|
911
1031
|
});
|
|
912
1032
|
}
|
|
1033
|
+
|
|
1034
|
+
if ("completion" in entry) {
|
|
1035
|
+
const completion = entry.completion;
|
|
1036
|
+
if (!completion || typeof completion !== "object" || Array.isArray(completion)) {
|
|
1037
|
+
issues.push({
|
|
1038
|
+
severity: machineEntry ? "error" : "warning",
|
|
1039
|
+
code: "LOG_COMPLETION_INVALID",
|
|
1040
|
+
message: "completion must be mapping",
|
|
1041
|
+
log: loc,
|
|
1042
|
+
});
|
|
1043
|
+
} else {
|
|
1044
|
+
if (eventType !== "status") {
|
|
1045
|
+
issues.push({
|
|
1046
|
+
severity: machineEntry ? "error" : "warning",
|
|
1047
|
+
code: "LOG_COMPLETION_EVENT_INVALID",
|
|
1048
|
+
message: "completion is only valid on status events",
|
|
1049
|
+
log: loc,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (!COMPLETION_ACCEPTANCE_VALUES.includes(completion.acceptance_criteria)) {
|
|
1054
|
+
issues.push({
|
|
1055
|
+
severity: machineEntry ? "error" : "warning",
|
|
1056
|
+
code: "LOG_COMPLETION_ACCEPTANCE_INVALID",
|
|
1057
|
+
message: `completion.acceptance_criteria must be one of ${COMPLETION_ACCEPTANCE_VALUES.join("|")}`,
|
|
1058
|
+
log: loc,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!COMPLETION_VERIFICATION_VALUES.includes(completion.verification)) {
|
|
1063
|
+
issues.push({
|
|
1064
|
+
severity: machineEntry ? "error" : "warning",
|
|
1065
|
+
code: "LOG_COMPLETION_VERIFICATION_INVALID",
|
|
1066
|
+
message: `completion.verification must be one of ${COMPLETION_VERIFICATION_VALUES.join("|")}`,
|
|
1067
|
+
log: loc,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const overrideRequired =
|
|
1072
|
+
completion.acceptance_criteria !== "met" || completion.verification !== "passed";
|
|
1073
|
+
const hasOverrideBy =
|
|
1074
|
+
typeof completion.overridden_by === "string" && completion.overridden_by.trim();
|
|
1075
|
+
const hasOverrideReason =
|
|
1076
|
+
typeof completion.override_reason === "string" && completion.override_reason.trim();
|
|
1077
|
+
const hasOverrideAt = completion.override_at !== undefined && completion.override_at !== null;
|
|
1078
|
+
|
|
1079
|
+
if (
|
|
1080
|
+
completion.overridden_by !== undefined &&
|
|
1081
|
+
completion.overridden_by !== null &&
|
|
1082
|
+
typeof completion.overridden_by !== "string"
|
|
1083
|
+
) {
|
|
1084
|
+
issues.push({
|
|
1085
|
+
severity: machineEntry ? "error" : "warning",
|
|
1086
|
+
code: "LOG_COMPLETION_OVERRIDE_BY_INVALID",
|
|
1087
|
+
message: "completion.overridden_by must be string or null",
|
|
1088
|
+
log: loc,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (
|
|
1093
|
+
completion.override_reason !== undefined &&
|
|
1094
|
+
completion.override_reason !== null &&
|
|
1095
|
+
typeof completion.override_reason !== "string"
|
|
1096
|
+
) {
|
|
1097
|
+
issues.push({
|
|
1098
|
+
severity: machineEntry ? "error" : "warning",
|
|
1099
|
+
code: "LOG_COMPLETION_OVERRIDE_REASON_INVALID",
|
|
1100
|
+
message: "completion.override_reason must be string or null",
|
|
1101
|
+
log: loc,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (hasOverrideAt && !parseIso(completion.override_at)) {
|
|
1106
|
+
issues.push({
|
|
1107
|
+
severity: machineEntry ? "error" : "warning",
|
|
1108
|
+
code: "LOG_COMPLETION_OVERRIDE_AT_INVALID",
|
|
1109
|
+
message: "completion.override_at must be ISO8601 UTC or null",
|
|
1110
|
+
log: loc,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (overrideRequired) {
|
|
1115
|
+
if (!hasOverrideBy) {
|
|
1116
|
+
issues.push({
|
|
1117
|
+
severity: machineEntry ? "error" : "warning",
|
|
1118
|
+
code: "LOG_COMPLETION_OVERRIDE_BY_MISSING",
|
|
1119
|
+
message: "completion.overridden_by required when completion gates are not fully satisfied",
|
|
1120
|
+
log: loc,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
if (!hasOverrideReason) {
|
|
1124
|
+
issues.push({
|
|
1125
|
+
severity: machineEntry ? "error" : "warning",
|
|
1126
|
+
code: "LOG_COMPLETION_OVERRIDE_REASON_MISSING",
|
|
1127
|
+
message: "completion.override_reason required when completion gates are not fully satisfied",
|
|
1128
|
+
log: loc,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
if (!hasOverrideAt) {
|
|
1132
|
+
issues.push({
|
|
1133
|
+
severity: machineEntry ? "error" : "warning",
|
|
1134
|
+
code: "LOG_COMPLETION_OVERRIDE_AT_MISSING",
|
|
1135
|
+
message: "completion.override_at required when completion gates are not fully satisfied",
|
|
1136
|
+
log: loc,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
} else if (hasOverrideBy || hasOverrideReason || hasOverrideAt) {
|
|
1140
|
+
issues.push({
|
|
1141
|
+
severity: machineEntry ? "error" : "warning",
|
|
1142
|
+
code: "LOG_COMPLETION_OVERRIDE_UNEXPECTED",
|
|
1143
|
+
message: "completion override fields are only valid when acceptance criteria or verification are not fully satisfied",
|
|
1144
|
+
log: loc,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
913
1149
|
});
|
|
914
1150
|
|
|
915
1151
|
return issues;
|