@4-r-c-4-n-4/todo 0.1.6 → 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.
- package/BIBLE.md +26 -3
- package/README.md +205 -2
- package/dist/branch-guard.js +9 -6
- package/dist/cli.js +2 -0
- package/dist/commands/close.js +46 -13
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.js +123 -0
- package/dist/commands/edit.js +1 -0
- package/dist/commands/new.js +2 -1
- package/dist/commands/scan.js +1 -0
- package/dist/commands/work.js +7 -3
- package/dist/config.d.ts +3 -1
- package/dist/config.js +13 -1
- package/dist/git.d.ts +11 -0
- package/dist/git.js +19 -0
- package/dist/state.js +13 -0
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/BIBLE.md
CHANGED
|
@@ -148,13 +148,32 @@ For `main`, enable:
|
|
|
148
148
|
|
|
149
149
|
- **Require a pull request before merging** — the agent stops at the PR; you click Merge.
|
|
150
150
|
- **Require status checks to pass** — your CI's `Lint, typecheck, test` job. Catches the kind of regression that this session's commit `b6709ed` (missing `pretest` hook) would have stopped on red.
|
|
151
|
-
- **Require linear history** — enforces the skill's `--no-ff` rule
|
|
151
|
+
- **Require linear history** — enforces the skill's `--no-ff` rule for *deliverable* commits, keeping the resolution-commit SHAs stored in `.todo/done/<id>.json` resolvable. (The `.todo/`-only state commits carry no such reference and are safe to squash or batch.)
|
|
152
152
|
- **Require approvals (optional)** — even for a solo repo, setting "1 approval" forces you to actually open the PR and skim the diff before clicking through. Cheap insurance against autopilot merges.
|
|
153
153
|
|
|
154
154
|
Once protected, the closing step becomes `todo pr` (push branch + open PR + stop) instead of a local merge. The local-merge sequence in the previous sections remains valid for unprotected repos or quick solo work.
|
|
155
155
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
Settings live in `.todo/config.json` (created by `todo init`). The keys that shape the git workflow:
|
|
161
|
+
|
|
162
|
+
| Key | Values | Default | Effect |
|
|
163
|
+
|-----|--------|---------|--------|
|
|
164
|
+
| `behavior.commit_prefix` | string | `todo:` | Prefix that ties commits to tickets |
|
|
165
|
+
| `behavior.branch_mode` | `per-ticket` \| `managed` | `per-ticket` | `per-ticket`: todo manages a `todo/<id>` branch and runs the branch guards. `managed`: **you** (or a PR flow) own branching — `todo work` does no git ops and `todo close` drops the branch guards and auto-records state |
|
|
166
|
+
| `behavior.guard_mode` | `advisory` \| `strict` | `advisory` | `advisory`: a failed branch/commit-prefix guard warns and proceeds. `strict`: it's a hard error (exit 1) — opt in when you want git to enforce the conventions |
|
|
167
|
+
|
|
168
|
+
**Pick a profile:**
|
|
169
|
+
- **PR-based / protected `main`** → `branch_mode: "managed"`. todo becomes a pure state-and-rationale tracker and leaves git entirely to you.
|
|
170
|
+
- **Agent-driven, todo owns the branches** → `branch_mode: "per-ticket"` + `guard_mode: "strict"` (this repo's own setting) to make the conventions enforced rather than suggested.
|
|
171
|
+
- **Default** (`per-ticket` + `advisory`) suits solo/local work: the structure is there, but a guard never blocks you.
|
|
172
|
+
|
|
173
|
+
`todo doctor` reconciles committed `.todo/` state against git reality (missing/unreachable resolution commits, done-parents with open children, active tickets whose branch is gone, misfiled tickets, dangling references). Run it in CI — it exits non-zero on errors (`--strict` also fails on warnings).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
158
177
|
## Ticket Types
|
|
159
178
|
|
|
160
179
|
| Type | When to use |
|
|
@@ -164,6 +183,7 @@ Once protected, the closing step becomes `todo pr` (push branch + open PR + stop
|
|
|
164
183
|
| `refactor` | Code change with no behavior change |
|
|
165
184
|
| `chore` | Tooling, config, dependency updates, cleanup |
|
|
166
185
|
| `debt` | Known-bad code that needs to be addressed later |
|
|
186
|
+
| `investigation` | A question to answer or decision to make; the deliverable is a documented conclusion, not code |
|
|
167
187
|
|
|
168
188
|
---
|
|
169
189
|
|
|
@@ -178,9 +198,12 @@ What's required before closing each ticket type:
|
|
|
178
198
|
| `refactor` | Commit SHA, tests still passing |
|
|
179
199
|
| `chore` | Commit SHA |
|
|
180
200
|
| `debt` | Commit SHA, note explaining what changed |
|
|
201
|
+
| `investigation` | A resolution **note** (the conclusion); commit optional, no test required |
|
|
181
202
|
|
|
182
203
|
Close command: `todo close <id> --note "what you did" --test tests/foo.ts::test_name`
|
|
183
204
|
|
|
205
|
+
`todo close --commit-state` records the `.todo/` state change in the same step, so a failed close can never be followed by a stray "close" commit. It's the recommended way to close.
|
|
206
|
+
|
|
184
207
|
---
|
|
185
208
|
|
|
186
209
|
## Commit Message Convention
|
|
@@ -203,9 +226,9 @@ The `<id>` is the full 8-char ticket ID. Always include it. This ties commits to
|
|
|
203
226
|
|
|
204
227
|
1. **Closing before committing** — `todo close` captures HEAD. If you haven't committed the fix, the resolution commit is wrong. Always commit code first.
|
|
205
228
|
|
|
206
|
-
2. **Forgetting `git add .todo/`** — Ticket files live in `.todo/`. They must be committed. After any `todo` command that writes, stage `.todo/`.
|
|
229
|
+
2. **Forgetting `git add .todo/`** — Ticket files live in `.todo/`. They must be committed. After any `todo` command that writes, stage `.todo/`. (`todo close --commit-state` does this for you atomically — see Workflow — which is the recommended way to avoid this footgun entirely.)
|
|
207
230
|
|
|
208
|
-
3. **Squash merging
|
|
231
|
+
3. **Squash merging can break commit refs** — A ticket's `resolution.commit` points at a real *code* commit. Squash-merging that commit replaces its SHA and orphans the reference, so never squash the deliverable commits — use `--no-ff` (or update the resolution commit after a squash). This does **not** apply to the `.todo/`-only state commits (`todo:<id> — close`, plan commits): nothing points at them — the resolution SHA lives *inside* the ticket file, not in `.todo/` history — so they may be squashed or batched (e.g. one `.todo/` commit per PR) to keep mainline history clean.
|
|
209
232
|
|
|
210
233
|
4. **Using a short prefix when IDs are ambiguous** — If two tickets share a prefix, commands fail. Use more characters of the ID.
|
|
211
234
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,207 @@
|
|
|
1
1
|
# todo
|
|
2
|
-
Making working on work work
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
**Git-native work tracking for coding agents.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@4-r-c-4-n-4/todo)
|
|
6
|
+
|
|
7
|
+
`todo` is a ticket tracker that lives *inside* your repository. Tickets are JSON files committed alongside your code in `.todo/`, so work state travels with the branch, survives clones, and is reviewable in a diff. It's designed so a coding agent can run the whole loop — capture a problem, attach reasoning, do the work, and close with proof — but it's just as usable by hand.
|
|
8
|
+
|
|
9
|
+
Two things make it more than a TODO list:
|
|
10
|
+
|
|
11
|
+
- **A rationale trail.** `todo analyze` attaches structured reasoning (blame → hypothesis → evidence → conclusion) to a ticket, turning it into a lightweight ADR that's there when you reopen the work months later.
|
|
12
|
+
- **A done contract.** A ticket can't close without proof — a resolution commit, and (for bugs) a test reference. Closes are self-documenting by construction.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @4-r-c-4-n-4/todo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires **Node ≥ 20** and a **git repository**. Then, from the repo root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
todo init # creates .todo/ and a config file
|
|
26
|
+
git add .todo && git commit -m "chore: init todo"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 1. Capture a problem
|
|
35
|
+
id=$(todo new "Login throws on empty password" --type bug --source human)
|
|
36
|
+
|
|
37
|
+
# 2. Start work (creates branch todo/<id>, marks the ticket active)
|
|
38
|
+
todo work "$id"
|
|
39
|
+
|
|
40
|
+
# 3. ...write the fix and a test, then commit with the ticket prefix
|
|
41
|
+
git commit -am "todo:$id — guard against empty password"
|
|
42
|
+
|
|
43
|
+
# 4. Close with proof, recording the .todo/ state in the same step
|
|
44
|
+
todo close "$id" \
|
|
45
|
+
--test tests/auth.test.ts::rejects_empty_password \
|
|
46
|
+
--note "Restored the null guard dropped in 3f9a1c2" \
|
|
47
|
+
--commit-state
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's the standalone-ticket loop. For multi-step features, see **Feature builds** below.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Core concepts
|
|
55
|
+
|
|
56
|
+
**Tickets** are JSON files under `.todo/open/` (live) and `.todo/done/` (terminal). Each has a stable 8-char id, a type, a state, a summary, and optional analysis, relationships, and resolution.
|
|
57
|
+
|
|
58
|
+
**States:** `open` → `active` → `done` (plus `blocked`, and the terminal `wontfix` / `duplicate`).
|
|
59
|
+
|
|
60
|
+
**Types and what it takes to close each (the done contract):**
|
|
61
|
+
|
|
62
|
+
| Type | Required to close |
|
|
63
|
+
|------|-------------------|
|
|
64
|
+
| `bug` | Commit + test file **and** function |
|
|
65
|
+
| `feature` | Commit + a test *or* a note |
|
|
66
|
+
| `refactor` | Commit |
|
|
67
|
+
| `chore` | Commit |
|
|
68
|
+
| `debt` | Commit (note recommended) |
|
|
69
|
+
| `investigation` | A **note** (the documented conclusion) — commit optional, no test needed |
|
|
70
|
+
| *parent* | A note, and every child in a terminal state |
|
|
71
|
+
|
|
72
|
+
`investigation` exists for work whose deliverable is a *decision*, not code — a benchmark, a design call, a "where should this live?" question. It closes on the conclusion.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Two workflow modes
|
|
77
|
+
|
|
78
|
+
`todo` does not force a branching workflow on you. Pick the one that matches your repo with `behavior.branch_mode`:
|
|
79
|
+
|
|
80
|
+
### `per-ticket` (default) — todo manages branches
|
|
81
|
+
|
|
82
|
+
`todo work <id>` creates `todo/<id>` (children share the parent's branch); you merge it back with `--no-ff` when done. Branch-convention guards run on `close`. Best for agent-driven repos where todo owns the git choreography.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
todo work <id>
|
|
86
|
+
git commit -am "todo:<id> — ..."
|
|
87
|
+
todo close <id> --note "..." --commit-state
|
|
88
|
+
git checkout main && git merge --no-ff todo/<id> && git branch -d todo/<id>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `managed` — you own branches (PRs, protected `main`)
|
|
92
|
+
|
|
93
|
+
`todo work` performs **no git operations**, and `close` drops the branch guards and records state automatically. `todo` becomes a pure state-and-rationale tracker and leaves git entirely to you and your PR flow.
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// .todo/config.json
|
|
97
|
+
{ "behavior": { "branch_mode": "managed" } }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git checkout -b feature/login-fix # your branch, your rules
|
|
102
|
+
todo work <id> # just marks the ticket active
|
|
103
|
+
# ...commit however you like...
|
|
104
|
+
todo close <id> --note "..." # no branch guard; state auto-committed
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Strict vs. advisory guards
|
|
108
|
+
|
|
109
|
+
In `per-ticket` mode, the branch-convention checks (are you on the right branch? does a commit reference this ticket?) are **advisory by default** — they warn and proceed. Set `behavior.guard_mode: "strict"` to make them hard errors (exit 1) when you want git to enforce the conventions. The real done contract (commit exists, test/note present) is always enforced.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Feature builds (parent + children)
|
|
114
|
+
|
|
115
|
+
Decompose a feature into an ordered parent + children. Children share the parent's branch and are worked in sequence.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
parent=$(todo new "OAuth2 login" --type feature --source agent)
|
|
119
|
+
c1=$(todo new "Add /auth/callback route" --type chore --parent "$parent")
|
|
120
|
+
c2=$(todo new "Validate session middleware" --type chore --parent "$parent")
|
|
121
|
+
|
|
122
|
+
todo work "$c1" # creates todo/<parent>
|
|
123
|
+
# ...implement, commit, close $c1 --commit-state...
|
|
124
|
+
todo next "$parent" # activates the next open child on the same branch
|
|
125
|
+
# ...repeat...
|
|
126
|
+
todo close "$parent" --note "All subtasks shipped" --commit-state
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Command reference
|
|
132
|
+
|
|
133
|
+
| Command | What it does |
|
|
134
|
+
|---------|--------------|
|
|
135
|
+
| `todo init` | Initialize `.todo/` in the current git repo |
|
|
136
|
+
| `todo new [summary]` | Create a ticket (`--type`, `--source`, `--parent`, `--tags`, `--file`, `--pipe`) |
|
|
137
|
+
| `todo list` | List tickets (`--state`, `--type`, `--tag`, `--file`, `--sort`, `--json`) |
|
|
138
|
+
| `todo show <id>` | Show ticket detail (`--raw` for JSON) |
|
|
139
|
+
| `todo edit <id>` | Edit fields (`--summary`, `--type`, `--tags`, `--parent`, …) |
|
|
140
|
+
| `todo work <id>` | Start/resume work (`--skip-branch`, `--branch`, `--actor`) |
|
|
141
|
+
| `todo next <parent>` | Activate the next open child on the current branch |
|
|
142
|
+
| `todo analyze <id>` | Append a reasoning entry (`--type`, `--content`, `--confidence`) |
|
|
143
|
+
| `todo transition <id> <state>` | Move a ticket to any state |
|
|
144
|
+
| `todo close <id>` | Close as done (`--commit`, `--test`, `--note`, `--commit-state`, `--force`) |
|
|
145
|
+
| `todo link <id> --to <target>` | Link to a commit, file, or ticket (`--relation`) |
|
|
146
|
+
| `todo scan` | Create tickets from `TODO`/`FIXME`/… comments in the tree |
|
|
147
|
+
| `todo dedup` | Find potential duplicate tickets |
|
|
148
|
+
| `todo doctor` | Reconcile `.todo/` against git reality and report drift |
|
|
149
|
+
| `todo export` | Dump tickets to stdout as JSON |
|
|
150
|
+
| `todo pr` | Push the `todo/<id>` branch and open/update a GitHub PR |
|
|
151
|
+
| `todo sync` | Push ticket state to a Hermes Kanban board |
|
|
152
|
+
| `todo install-hooks` | Install git hooks that enforce the conventions |
|
|
153
|
+
|
|
154
|
+
Run `todo <command> --help` for full options.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
Settings live in `.todo/config.json` (created by `todo init`). The keys that shape the workflow:
|
|
161
|
+
|
|
162
|
+
| Key | Values | Default | Effect |
|
|
163
|
+
|-----|--------|---------|--------|
|
|
164
|
+
| `behavior.commit_prefix` | string | `todo:` | Prefix that ties commits to tickets |
|
|
165
|
+
| `behavior.branch_mode` | `per-ticket` \| `managed` | `per-ticket` | Whether todo manages branches or leaves git to you (see above) |
|
|
166
|
+
| `behavior.guard_mode` | `advisory` \| `strict` | `advisory` | Whether failed branch guards warn or hard-fail |
|
|
167
|
+
| `intake.scan_patterns` | string[] | `TODO, FIXME, HACK, XXX` | Comment markers `todo scan` picks up |
|
|
168
|
+
| `intake.dedup_strategy` | `fingerprint` \| `file-line` \| `semantic` | `fingerprint` | How duplicates are detected |
|
|
169
|
+
| `display.id_length` | number | `8` | Characters of the id shown in lists |
|
|
170
|
+
| `display.date_format` | `relative` \| `iso` | `relative` | Date rendering |
|
|
171
|
+
|
|
172
|
+
**Profiles at a glance:**
|
|
173
|
+
- **PR-based / protected `main`** → `branch_mode: "managed"`.
|
|
174
|
+
- **Agent-driven, todo owns branches** → `branch_mode: "per-ticket"` + `guard_mode: "strict"`.
|
|
175
|
+
- **Solo / local** → the defaults: structure is there, but a guard never blocks you.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## `todo doctor`
|
|
180
|
+
|
|
181
|
+
Because state lives in git and is edited by agents, the `.todo/` store can drift from reality. `todo doctor` reconciles them and reports:
|
|
182
|
+
|
|
183
|
+
- done tickets whose resolution commit is missing (orphaned by a squash/rebase) or unreachable from `HEAD`
|
|
184
|
+
- done parents with a still-open child
|
|
185
|
+
- active tickets whose branch was deleted
|
|
186
|
+
- tickets misfiled in the wrong directory for their state
|
|
187
|
+
- dangling parent/child/dependency references
|
|
188
|
+
|
|
189
|
+
It exits non-zero on errors (and on warnings too with `--strict`), so it drops straight into CI. `--json` for machine output.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## For coding agents
|
|
194
|
+
|
|
195
|
+
`todo` ships a set of **skills** (under `skills/`) that drive the lifecycle end-to-end: `todo-capture` (intake any signal into a ticket), `todo-triage` (classify raw captures), `todo-plan` (decompose a feature), and `todo-implement` (write the code and close with proof). They encode the conventions so an agent follows them reliably.
|
|
196
|
+
|
|
197
|
+
`TODO_ACTOR` sets the identity recorded on transitions when git's `user.name` isn't the right attribution.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Learn more
|
|
202
|
+
|
|
203
|
+
[**BIBLE.md**](./BIBLE.md) is the full doctrine — the lifecycle, the branch workflow, branch-protection setup, and the reasoning behind the done contract.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/branch-guard.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Branch-convention guards used by `todo close` and `todo work`.
|
|
3
3
|
//
|
|
4
|
-
// These
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// These encode the conventions documented in the todo-implement skill. They
|
|
5
|
+
// are *advisory by default* — the caller warns and proceeds — because a
|
|
6
|
+
// standalone tool should not litigate a branching workflow its user did not
|
|
7
|
+
// choose. A project that wants git to enforce the conventions opts in with
|
|
8
|
+
// behavior.guard_mode = "strict", which turns these into hard preconditions
|
|
9
|
+
// (agents follow exit codes far more reliably than prose). managed branch_mode
|
|
10
|
+
// skips them entirely. Each function therefore just *reports* a check result;
|
|
11
|
+
// the severity decision lives in the command.
|
|
7
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
13
|
exports.expectedBranchFor = expectedBranchFor;
|
|
9
14
|
exports.checkOnExpectedBranch = checkOnExpectedBranch;
|
|
@@ -39,9 +44,7 @@ function checkBranchHasTodoCommit(ticket, repoRoot, commitPrefix) {
|
|
|
39
44
|
return { ok: true };
|
|
40
45
|
return {
|
|
41
46
|
ok: false,
|
|
42
|
-
message: `
|
|
43
|
-
` Either amend a commit to include the prefix, or pass --force ` +
|
|
44
|
-
`if this ticket genuinely has no code change attached.`,
|
|
47
|
+
message: `no commit since ${base} has a message containing '${needle}'.`,
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const commander_1 = require("commander");
|
|
|
7
7
|
const analyze_js_1 = require("./commands/analyze.js");
|
|
8
8
|
const close_js_1 = require("./commands/close.js");
|
|
9
9
|
const dedup_js_1 = require("./commands/dedup.js");
|
|
10
|
+
const doctor_js_1 = require("./commands/doctor.js");
|
|
10
11
|
const edit_js_1 = require("./commands/edit.js");
|
|
11
12
|
const export_js_1 = require("./commands/export.js");
|
|
12
13
|
const init_js_1 = require("./commands/init.js");
|
|
@@ -46,4 +47,5 @@ program
|
|
|
46
47
|
(0, install_hooks_js_1.registerInstallHooks)(program);
|
|
47
48
|
(0, sync_js_1.registerSync)(program);
|
|
48
49
|
(0, pr_js_1.registerPr)(program);
|
|
50
|
+
(0, doctor_js_1.registerDoctor)(program);
|
|
49
51
|
program.parse(process.argv);
|
package/dist/commands/close.js
CHANGED
|
@@ -16,14 +16,31 @@ function registerClose(program) {
|
|
|
16
16
|
.option("--test <file::func>", "test file and function (colon-separated)")
|
|
17
17
|
.option("--note <text>", "resolution note")
|
|
18
18
|
.option("--checkout", "git checkout base_branch after closing")
|
|
19
|
+
.option("--commit-state", "atomically git-commit the .todo/ state change (no separate manual commit needed)")
|
|
19
20
|
.option("--force", "skip branch and commit-message guards")
|
|
20
21
|
.action((id, opts) => {
|
|
21
22
|
const ctx = (0, context_js_1.getContext)(true);
|
|
22
23
|
const { repoRoot, config } = ctx;
|
|
23
24
|
try {
|
|
24
25
|
const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const managed = (0, config_js_1.getBranchMode)(config) === "managed";
|
|
27
|
+
const strict = (0, config_js_1.getGuardMode)(config) === "strict";
|
|
28
|
+
// Branch-convention guards. Skipped entirely in managed mode (the
|
|
29
|
+
// user owns branching) or with --force. Otherwise they are advisory
|
|
30
|
+
// by default — warn and proceed — and only hard-fail when the
|
|
31
|
+
// project opts into behavior.guard_mode = "strict". This keeps the
|
|
32
|
+
// tool from litigating a workflow you are not using, while still
|
|
33
|
+
// letting agent-driven repos enforce the conventions.
|
|
34
|
+
if (!opts.force && !managed) {
|
|
35
|
+
// Report a guard failure at the configured severity. In strict
|
|
36
|
+
// mode it exits 1; otherwise it warns and execution continues.
|
|
37
|
+
const reportGuard = (message) => {
|
|
38
|
+
if (strict) {
|
|
39
|
+
console.error(`Error: ${message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.error(`Warning: ${message} (advisory — proceeding)`);
|
|
43
|
+
};
|
|
27
44
|
let currentBranch = "";
|
|
28
45
|
try {
|
|
29
46
|
currentBranch = (0, git_js_1.getCurrentBranch)(repoRoot);
|
|
@@ -32,22 +49,21 @@ function registerClose(program) {
|
|
|
32
49
|
// Detached HEAD or other; treat as a branch mismatch.
|
|
33
50
|
}
|
|
34
51
|
const branchCheck = (0, branch_guard_js_1.checkOnExpectedBranch)(ticket, currentBranch);
|
|
35
|
-
if (!branchCheck.ok)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
if (!(0, branch_guard_js_1.isParentWithAllChildrenClosed)(ticket, repoRoot)) {
|
|
52
|
+
if (!branchCheck.ok)
|
|
53
|
+
reportGuard(branchCheck.message ?? "");
|
|
54
|
+
// The commit-prefix grep is a heuristic over commit messages, not
|
|
55
|
+
// the real done-contract (commit-exists + test/note, enforced in
|
|
56
|
+
// state.ts). An explicit --commit names the deliverable, so skip
|
|
57
|
+
// it; a parent with all children closed carries no commit of its
|
|
58
|
+
// own.
|
|
59
|
+
if (!opts.commit && !(0, branch_guard_js_1.isParentWithAllChildrenClosed)(ticket, repoRoot)) {
|
|
43
60
|
const commitCheck = (0, branch_guard_js_1.checkBranchHasTodoCommit)(ticket, repoRoot, (0, config_js_1.getCommitPrefix)(config));
|
|
44
61
|
if (!commitCheck.ok) {
|
|
45
|
-
|
|
46
|
-
process.exit(1);
|
|
62
|
+
reportGuard(`${commitCheck.message} Pass --commit <sha> to point close at the deliverable`);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
|
-
else {
|
|
66
|
+
else if (opts.force) {
|
|
51
67
|
console.error("Warning: --force used; skipping branch guards.");
|
|
52
68
|
}
|
|
53
69
|
// Resolve commit
|
|
@@ -88,6 +104,23 @@ function registerClose(program) {
|
|
|
88
104
|
}
|
|
89
105
|
(0, ticket_js_1.writeTicket)(repoRoot, updated);
|
|
90
106
|
console.log(`Closed ${updated.id}`);
|
|
107
|
+
// Atomically record the state change. This only runs once the
|
|
108
|
+
// close has already succeeded on disk, so the "close" commit can
|
|
109
|
+
// never describe a ticket that did not actually close — the
|
|
110
|
+
// phantom-commit hazard of a manual `git commit` after a failed
|
|
111
|
+
// close. Runs before --checkout so the commit lands on the ticket
|
|
112
|
+
// branch before switching away.
|
|
113
|
+
if (opts.commitState || managed) {
|
|
114
|
+
const message = `${(0, config_js_1.getCommitPrefix)(config)}${updated.id} — close`;
|
|
115
|
+
try {
|
|
116
|
+
(0, git_js_1.commitTodoState)(message, repoRoot);
|
|
117
|
+
console.log(`Recorded .todo/ state: ${message}`);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`Warning: closed ${updated.id} but could not commit .todo/ state: ${err.message}\n` +
|
|
121
|
+
" Commit it manually: git add -A .todo && git commit");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
91
124
|
// Checkout base branch if requested
|
|
92
125
|
if (opts.checkout) {
|
|
93
126
|
const targetBranch = updated.work?.base_branch ?? (0, git_js_1.getDefaultBranch)(repoRoot);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type Severity = "error" | "warning";
|
|
3
|
+
interface Issue {
|
|
4
|
+
severity: Severity;
|
|
5
|
+
ticket: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reconcile committed .todo/ state against git reality and report drift.
|
|
10
|
+
* Because state lives in git and is edited by agents, the file can silently
|
|
11
|
+
* diverge from what actually happened (a done ticket whose resolution commit
|
|
12
|
+
* was never merged, an active ticket whose branch was deleted, a ticket in
|
|
13
|
+
* the wrong directory for its state). `doctor` turns that into a report.
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectIssues(repoRoot: string): Issue[];
|
|
16
|
+
export declare function registerDoctor(program: Command): void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectIssues = collectIssues;
|
|
4
|
+
exports.registerDoctor = registerDoctor;
|
|
5
|
+
const context_js_1 = require("../context.js");
|
|
6
|
+
const errors_js_1 = require("../errors.js");
|
|
7
|
+
const git_js_1 = require("../git.js");
|
|
8
|
+
const ticket_js_1 = require("../ticket.js");
|
|
9
|
+
/**
|
|
10
|
+
* Reconcile committed .todo/ state against git reality and report drift.
|
|
11
|
+
* Because state lives in git and is edited by agents, the file can silently
|
|
12
|
+
* diverge from what actually happened (a done ticket whose resolution commit
|
|
13
|
+
* was never merged, an active ticket whose branch was deleted, a ticket in
|
|
14
|
+
* the wrong directory for its state). `doctor` turns that into a report.
|
|
15
|
+
*/
|
|
16
|
+
function collectIssues(repoRoot) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
const add = (severity, ticket, message) => issues.push({ severity, ticket, message });
|
|
19
|
+
const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
|
|
20
|
+
const doneTickets = (0, ticket_js_1.listTickets)(repoRoot, "done");
|
|
21
|
+
const all = [...openTickets, ...doneTickets];
|
|
22
|
+
const byId = new Map(all.map((t) => [t.id, t]));
|
|
23
|
+
let head;
|
|
24
|
+
try {
|
|
25
|
+
head = (0, git_js_1.resolveHEAD)(repoRoot);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
head = undefined;
|
|
29
|
+
}
|
|
30
|
+
// Directory must agree with state: open/ holds non-terminal, done/ terminal.
|
|
31
|
+
for (const t of openTickets) {
|
|
32
|
+
if (ticket_js_1.TERMINAL_STATES.includes(t.state)) {
|
|
33
|
+
add("error", t.id, `is in .todo/open/ but its state is '${t.state}' (terminal) — file is in the wrong directory`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const t of doneTickets) {
|
|
37
|
+
if (!ticket_js_1.TERMINAL_STATES.includes(t.state)) {
|
|
38
|
+
add("error", t.id, `is in .todo/done/ but its state is '${t.state}' (non-terminal) — file is in the wrong directory`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const t of all) {
|
|
42
|
+
// Resolution integrity for done tickets.
|
|
43
|
+
if (t.state === "done") {
|
|
44
|
+
const sha = t.resolution?.commit;
|
|
45
|
+
if (!sha) {
|
|
46
|
+
add("error", t.id, "is done but records no resolution commit");
|
|
47
|
+
}
|
|
48
|
+
else if (!(0, git_js_1.commitExists)(sha, repoRoot)) {
|
|
49
|
+
add("error", t.id, `resolution commit ${sha} does not exist in the repository (orphaned by a squash/rebase?)`);
|
|
50
|
+
}
|
|
51
|
+
else if (head && !(0, git_js_1.isAncestor)(sha, "HEAD", repoRoot)) {
|
|
52
|
+
add("warning", t.id, `resolution commit ${sha.slice(0, 8)} is not reachable from HEAD (closed on an unmerged branch?)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// A done parent must have no non-terminal children.
|
|
56
|
+
const children = t.relationships?.children ?? [];
|
|
57
|
+
if (ticket_js_1.TERMINAL_STATES.includes(t.state) && children.length > 0) {
|
|
58
|
+
for (const childId of children) {
|
|
59
|
+
const child = byId.get(childId);
|
|
60
|
+
if (child && !ticket_js_1.TERMINAL_STATES.includes(child.state)) {
|
|
61
|
+
add("error", t.id, `is ${t.state} but child ${childId} is still '${child.state}'`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// An active ticket whose branch was deleted has lost its working context.
|
|
66
|
+
if (t.state === "active" && t.work?.branch) {
|
|
67
|
+
if (!(0, git_js_1.branchExists)(t.work.branch, repoRoot)) {
|
|
68
|
+
add("warning", t.id, `is active but its branch '${t.work.branch}' no longer exists`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Dangling relationship references.
|
|
72
|
+
const rel = t.relationships;
|
|
73
|
+
if (rel?.parent && !byId.has(rel.parent)) {
|
|
74
|
+
add("warning", t.id, `references missing parent ${rel.parent}`);
|
|
75
|
+
}
|
|
76
|
+
for (const childId of children) {
|
|
77
|
+
if (!byId.has(childId)) {
|
|
78
|
+
add("warning", t.id, `references missing child ${childId}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const depId of rel?.depends_on ?? []) {
|
|
82
|
+
if (!byId.has(depId)) {
|
|
83
|
+
add("warning", t.id, `references missing dependency ${depId}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return issues;
|
|
88
|
+
}
|
|
89
|
+
function registerDoctor(program) {
|
|
90
|
+
program
|
|
91
|
+
.command("doctor")
|
|
92
|
+
.description("Reconcile committed .todo/ state against git reality and report drift")
|
|
93
|
+
.option("--json", "output issues as a JSON array")
|
|
94
|
+
.option("--strict", "exit non-zero when any issue is found, including warnings")
|
|
95
|
+
.action((opts) => {
|
|
96
|
+
const ctx = (0, context_js_1.getContext)(true);
|
|
97
|
+
const { repoRoot } = ctx;
|
|
98
|
+
try {
|
|
99
|
+
const issues = collectIssues(repoRoot);
|
|
100
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
101
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
102
|
+
if (opts.json) {
|
|
103
|
+
console.log(JSON.stringify(issues, null, 2));
|
|
104
|
+
}
|
|
105
|
+
else if (issues.length === 0) {
|
|
106
|
+
console.log("todo doctor: no issues found — .todo/ is consistent.");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
for (const i of issues) {
|
|
110
|
+
const tag = i.severity === "error" ? "ERROR" : "warn ";
|
|
111
|
+
console.log(`${tag} ${i.ticket} ${i.message}`);
|
|
112
|
+
}
|
|
113
|
+
console.log(`\n${errors.length} error(s), ${warnings.length} warning(s).`);
|
|
114
|
+
}
|
|
115
|
+
const failed = opts.strict ? issues.length > 0 : errors.length > 0;
|
|
116
|
+
if (failed)
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
(0, errors_js_1.handleError)(err);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
package/dist/commands/edit.js
CHANGED
package/dist/commands/new.js
CHANGED
|
@@ -12,6 +12,7 @@ const VALID_TYPES = [
|
|
|
12
12
|
"refactor",
|
|
13
13
|
"chore",
|
|
14
14
|
"debt",
|
|
15
|
+
"investigation",
|
|
15
16
|
];
|
|
16
17
|
const VALID_SOURCES = [
|
|
17
18
|
"log",
|
|
@@ -24,7 +25,7 @@ function registerNew(program) {
|
|
|
24
25
|
program
|
|
25
26
|
.command("new [summary]")
|
|
26
27
|
.description("Create a new ticket")
|
|
27
|
-
.option("-t, --type <type>", "ticket type: bug|feature|refactor|chore|debt", "chore")
|
|
28
|
+
.option("-t, --type <type>", "ticket type: bug|feature|refactor|chore|debt|investigation", "chore")
|
|
28
29
|
.option("-s, --source <source>", "source type: log|test|agent|human|comment", "human")
|
|
29
30
|
.option("-f, --file <path>", "associate a file path")
|
|
30
31
|
.option("-l, --lines <start,end>", "line range for the file (e.g. 10,20)")
|
package/dist/commands/scan.js
CHANGED
package/dist/commands/work.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerWork = registerWork;
|
|
4
4
|
const branch_guard_js_1 = require("../branch-guard.js");
|
|
5
|
+
const config_js_1 = require("../config.js");
|
|
5
6
|
const context_js_1 = require("../context.js");
|
|
6
7
|
const errors_js_1 = require("../errors.js");
|
|
7
8
|
const git_js_1 = require("../git.js");
|
|
@@ -16,9 +17,12 @@ function registerWork(program) {
|
|
|
16
17
|
.option("--actor <name>", "override actor (also reads TODO_ACTOR env)")
|
|
17
18
|
.action((id, opts) => {
|
|
18
19
|
const ctx = (0, context_js_1.getContext)(true);
|
|
19
|
-
const { repoRoot } = ctx;
|
|
20
|
+
const { repoRoot, config } = ctx;
|
|
20
21
|
try {
|
|
21
22
|
const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
|
|
23
|
+
// In managed branch_mode the user (or a PR flow) owns branching,
|
|
24
|
+
// so `work` performs no git ops — same as passing --skip-branch.
|
|
25
|
+
const skipBranch = opts.skipBranch || (0, config_js_1.getBranchMode)(config) === "managed";
|
|
22
26
|
// Check not terminal
|
|
23
27
|
if (ticket_js_1.TERMINAL_STATES.includes(ticket.state)) {
|
|
24
28
|
console.error(`Error: ticket ${ticket.id} is in terminal state '${ticket.state}'. Cannot work on it.`);
|
|
@@ -55,7 +59,7 @@ function registerWork(program) {
|
|
|
55
59
|
// Dirty-tree guard: refuse to leave a different todo/* branch
|
|
56
60
|
// with uncommitted work. We let `main`/other branches through —
|
|
57
61
|
// only the cross-ticket case is the one that loses work.
|
|
58
|
-
if (!
|
|
62
|
+
if (!skipBranch) {
|
|
59
63
|
let currentBranch = "";
|
|
60
64
|
try {
|
|
61
65
|
currentBranch = (0, git_js_1.getCurrentBranch)(repoRoot);
|
|
@@ -73,7 +77,7 @@ function registerWork(program) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
|
-
if (
|
|
80
|
+
if (skipBranch) {
|
|
77
81
|
// --no-branch: orchestrator mode — activate on current branch without any git ops
|
|
78
82
|
const currentBranch = (0, git_js_1.getCurrentBranch)(repoRoot);
|
|
79
83
|
if (ticket.state !== "active") {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { Config } from "./types.js";
|
|
1
|
+
import type { BranchMode, Config, GuardMode } from "./types.js";
|
|
2
2
|
export declare const DEFAULT_CONFIG: Config;
|
|
3
3
|
export declare function loadConfig(repoRoot: string): Config;
|
|
4
4
|
export declare function getTodoDir(repoRoot: string): string;
|
|
5
5
|
export declare function ensureTodoDir(repoRoot: string): void;
|
|
6
6
|
export declare function getIdLength(config: Config): number;
|
|
7
7
|
export declare function getCommitPrefix(config: Config): string;
|
|
8
|
+
export declare function getBranchMode(config: Config): BranchMode;
|
|
9
|
+
export declare function getGuardMode(config: Config): GuardMode;
|
package/dist/config.js
CHANGED
|
@@ -7,11 +7,17 @@ exports.getTodoDir = getTodoDir;
|
|
|
7
7
|
exports.ensureTodoDir = ensureTodoDir;
|
|
8
8
|
exports.getIdLength = getIdLength;
|
|
9
9
|
exports.getCommitPrefix = getCommitPrefix;
|
|
10
|
+
exports.getBranchMode = getBranchMode;
|
|
11
|
+
exports.getGuardMode = getGuardMode;
|
|
10
12
|
const node_fs_1 = require("node:fs");
|
|
11
13
|
const node_path_1 = require("node:path");
|
|
12
14
|
exports.DEFAULT_CONFIG = {
|
|
13
15
|
project: { name: "" },
|
|
14
|
-
behavior: {
|
|
16
|
+
behavior: {
|
|
17
|
+
commit_prefix: "todo:",
|
|
18
|
+
branch_mode: "per-ticket",
|
|
19
|
+
guard_mode: "advisory",
|
|
20
|
+
},
|
|
15
21
|
intake: {
|
|
16
22
|
dedup_strategy: "fingerprint",
|
|
17
23
|
scan_patterns: ["TODO", "FIXME", "HACK", "XXX"],
|
|
@@ -75,3 +81,9 @@ function getIdLength(config) {
|
|
|
75
81
|
function getCommitPrefix(config) {
|
|
76
82
|
return config.behavior?.commit_prefix ?? "todo:";
|
|
77
83
|
}
|
|
84
|
+
function getBranchMode(config) {
|
|
85
|
+
return config.behavior?.branch_mode ?? "per-ticket";
|
|
86
|
+
}
|
|
87
|
+
function getGuardMode(config) {
|
|
88
|
+
return config.behavior?.guard_mode ?? "advisory";
|
|
89
|
+
}
|
package/dist/git.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export declare function getLastCommitForFile(path: string, cwd?: string): string
|
|
|
17
17
|
export declare function createBranch(name: string, cwd?: string): void;
|
|
18
18
|
export declare function checkoutBranch(name: string, cwd?: string): void;
|
|
19
19
|
export declare function getGitUserName(cwd?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Stage the .todo/ directory (additions, modifications, AND deletions — a
|
|
22
|
+
* close moves the file from open/ to done/) and commit it. Used by
|
|
23
|
+
* `todo close --commit-state` to make close-and-record atomic: the state
|
|
24
|
+
* file is recorded by the same command that closed the ticket, so a failed
|
|
25
|
+
* close can never be followed by a stray manual "close" commit that desyncs
|
|
26
|
+
* committed .todo/ from reality. Returns the resulting HEAD sha. If nothing
|
|
27
|
+
* is staged (state already committed), no commit is made and current HEAD is
|
|
28
|
+
* returned — a close that already succeeded on disk must not error out here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commitTodoState(message: string, cwd?: string): string;
|
|
20
31
|
export declare function hasUncommittedChanges(cwd?: string): boolean;
|
|
21
32
|
export declare function getCommitMessagesBetween(base: string, head: string, cwd?: string): string[];
|
|
22
33
|
export declare function getGitDir(cwd?: string): string;
|
package/dist/git.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.getLastCommitForFile = getLastCommitForFile;
|
|
|
17
17
|
exports.createBranch = createBranch;
|
|
18
18
|
exports.checkoutBranch = checkoutBranch;
|
|
19
19
|
exports.getGitUserName = getGitUserName;
|
|
20
|
+
exports.commitTodoState = commitTodoState;
|
|
20
21
|
exports.hasUncommittedChanges = hasUncommittedChanges;
|
|
21
22
|
exports.getCommitMessagesBetween = getCommitMessagesBetween;
|
|
22
23
|
exports.getGitDir = getGitDir;
|
|
@@ -120,6 +121,24 @@ function checkoutBranch(name, cwd = process.cwd()) {
|
|
|
120
121
|
function getGitUserName(cwd = process.cwd()) {
|
|
121
122
|
return exec(["config", "user.name"], cwd);
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Stage the .todo/ directory (additions, modifications, AND deletions — a
|
|
126
|
+
* close moves the file from open/ to done/) and commit it. Used by
|
|
127
|
+
* `todo close --commit-state` to make close-and-record atomic: the state
|
|
128
|
+
* file is recorded by the same command that closed the ticket, so a failed
|
|
129
|
+
* close can never be followed by a stray manual "close" commit that desyncs
|
|
130
|
+
* committed .todo/ from reality. Returns the resulting HEAD sha. If nothing
|
|
131
|
+
* is staged (state already committed), no commit is made and current HEAD is
|
|
132
|
+
* returned — a close that already succeeded on disk must not error out here.
|
|
133
|
+
*/
|
|
134
|
+
function commitTodoState(message, cwd = process.cwd()) {
|
|
135
|
+
exec(["add", "-A", ".todo"], cwd);
|
|
136
|
+
const staged = exec(["diff", "--cached", "--name-only", "--", ".todo"], cwd);
|
|
137
|
+
if (staged.length === 0)
|
|
138
|
+
return resolveHEAD(cwd);
|
|
139
|
+
exec(["commit", "-m", message], cwd);
|
|
140
|
+
return resolveHEAD(cwd);
|
|
141
|
+
}
|
|
123
142
|
function hasUncommittedChanges(cwd = process.cwd()) {
|
|
124
143
|
const out = exec(["status", "--porcelain"], cwd);
|
|
125
144
|
return out.length > 0;
|
package/dist/state.js
CHANGED
|
@@ -58,6 +58,19 @@ function validateTransition(ticket, targetState, params, repoRoot) {
|
|
|
58
58
|
}
|
|
59
59
|
// commit is optional for parent (defaults to HEAD — caller handles HEAD resolution)
|
|
60
60
|
}
|
|
61
|
+
else if (ticket.type === "investigation") {
|
|
62
|
+
// Investigation/decision: the deliverable is a documented conclusion,
|
|
63
|
+
// not a code change. Require a resolution note (the conclusion, which
|
|
64
|
+
// should reference any doc/finding). A commit is optional — close
|
|
65
|
+
// still defaults it to HEAD for provenance — but if one is given it
|
|
66
|
+
// must exist.
|
|
67
|
+
if (!params.note) {
|
|
68
|
+
throw new Error(`Investigation ticket requires a resolution note (the documented conclusion)`);
|
|
69
|
+
}
|
|
70
|
+
if (commit && !(0, git_js_1.commitExists)(commit, repoRoot)) {
|
|
71
|
+
throw new Error(`Commit '${commit}' does not exist in the repository`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
61
74
|
else {
|
|
62
75
|
// Non-parent: commit always required
|
|
63
76
|
if (!commit) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export type TicketType = "bug" | "feature" | "refactor" | "chore" | "debt";
|
|
1
|
+
export type TicketType = "bug" | "feature" | "refactor" | "chore" | "debt" | "investigation";
|
|
2
2
|
export type State = "open" | "active" | "blocked" | "done" | "wontfix" | "duplicate";
|
|
3
3
|
export type SourceType = "log" | "test" | "agent" | "human" | "comment";
|
|
4
|
+
export type BranchMode = "per-ticket" | "managed";
|
|
5
|
+
export type GuardMode = "advisory" | "strict";
|
|
4
6
|
export type AnalysisType = "blame" | "hypothesis" | "evidence" | "conclusion";
|
|
5
7
|
export type Source = {
|
|
6
8
|
type: "log";
|
|
@@ -87,6 +89,8 @@ export interface Config {
|
|
|
87
89
|
};
|
|
88
90
|
behavior?: {
|
|
89
91
|
commit_prefix?: string;
|
|
92
|
+
branch_mode?: BranchMode;
|
|
93
|
+
guard_mode?: GuardMode;
|
|
90
94
|
};
|
|
91
95
|
intake?: {
|
|
92
96
|
dedup_strategy?: "fingerprint" | "file-line" | "semantic";
|