@4-r-c-4-n-4/todo 0.1.2

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.
Files changed (55) hide show
  1. package/BIBLE.md +212 -0
  2. package/LICENSE +21 -0
  3. package/README.md +2 -0
  4. package/dist/bundle.js +5109 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +36 -0
  7. package/dist/commands/analyze.d.ts +2 -0
  8. package/dist/commands/analyze.js +77 -0
  9. package/dist/commands/close.d.ts +2 -0
  10. package/dist/commands/close.js +75 -0
  11. package/dist/commands/dedup.d.ts +2 -0
  12. package/dist/commands/dedup.js +74 -0
  13. package/dist/commands/edit.d.ts +2 -0
  14. package/dist/commands/edit.js +82 -0
  15. package/dist/commands/export.d.ts +2 -0
  16. package/dist/commands/export.js +25 -0
  17. package/dist/commands/init.d.ts +2 -0
  18. package/dist/commands/init.js +29 -0
  19. package/dist/commands/link.d.ts +2 -0
  20. package/dist/commands/link.js +101 -0
  21. package/dist/commands/list.d.ts +2 -0
  22. package/dist/commands/list.js +91 -0
  23. package/dist/commands/new.d.ts +2 -0
  24. package/dist/commands/new.js +157 -0
  25. package/dist/commands/scan.d.ts +2 -0
  26. package/dist/commands/scan.js +114 -0
  27. package/dist/commands/show.d.ts +2 -0
  28. package/dist/commands/show.js +132 -0
  29. package/dist/commands/transition.d.ts +2 -0
  30. package/dist/commands/transition.js +66 -0
  31. package/dist/commands/work.d.ts +2 -0
  32. package/dist/commands/work.js +128 -0
  33. package/dist/config.d.ts +7 -0
  34. package/dist/config.js +77 -0
  35. package/dist/context.d.ts +7 -0
  36. package/dist/context.js +24 -0
  37. package/dist/dedup.d.ts +8 -0
  38. package/dist/dedup.js +63 -0
  39. package/dist/errors.d.ts +1 -0
  40. package/dist/errors.js +26 -0
  41. package/dist/fingerprint.d.ts +9 -0
  42. package/dist/fingerprint.js +27 -0
  43. package/dist/format.d.ts +14 -0
  44. package/dist/format.js +44 -0
  45. package/dist/git.d.ts +19 -0
  46. package/dist/git.js +115 -0
  47. package/dist/scan.d.ts +7 -0
  48. package/dist/scan.js +146 -0
  49. package/dist/state.d.ts +13 -0
  50. package/dist/state.js +108 -0
  51. package/dist/ticket.d.ts +23 -0
  52. package/dist/ticket.js +143 -0
  53. package/dist/types.d.ts +94 -0
  54. package/dist/types.js +3 -0
  55. package/package.json +51 -0
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerNew = registerNew;
4
+ const node_fs_1 = require("node:fs");
5
+ const config_js_1 = require("../config.js");
6
+ const context_js_1 = require("../context.js");
7
+ const fingerprint_js_1 = require("../fingerprint.js");
8
+ const ticket_js_1 = require("../ticket.js");
9
+ const VALID_TYPES = [
10
+ "bug",
11
+ "feature",
12
+ "refactor",
13
+ "chore",
14
+ "debt",
15
+ ];
16
+ const VALID_SOURCES = [
17
+ "log",
18
+ "test",
19
+ "agent",
20
+ "human",
21
+ "comment",
22
+ ];
23
+ function registerNew(program) {
24
+ program
25
+ .command("new [summary]")
26
+ .description("Create a new ticket")
27
+ .option("-t, --type <type>", "ticket type: bug|feature|refactor|chore|debt", "chore")
28
+ .option("-s, --source <source>", "source type: log|test|agent|human|comment", "human")
29
+ .option("-f, --file <path>", "associate a file path")
30
+ .option("-l, --lines <start,end>", "line range for the file (e.g. 10,20)")
31
+ .option("--tags <tags>", "comma-separated tags")
32
+ .option("--parent <id>", "parent ticket ID")
33
+ .option("--pipe", "read summary from stdin")
34
+ .action((summaryArg, opts) => {
35
+ // Validate type
36
+ const ticketType = opts.type;
37
+ if (!VALID_TYPES.includes(ticketType)) {
38
+ console.error(`Error: invalid type '${ticketType}'. Must be one of: ${VALID_TYPES.join(", ")}`);
39
+ process.exit(1);
40
+ }
41
+ // Validate source
42
+ const sourceType = opts.source;
43
+ if (!VALID_SOURCES.includes(sourceType)) {
44
+ console.error(`Error: invalid source '${sourceType}'. Must be one of: ${VALID_SOURCES.join(", ")}`);
45
+ process.exit(1);
46
+ }
47
+ // Get context (init required)
48
+ const ctx = (0, context_js_1.getContext)(true);
49
+ const { repoRoot } = ctx;
50
+ // Ensure dirs exist
51
+ (0, config_js_1.ensureTodoDir)(repoRoot);
52
+ // Handle --pipe
53
+ let summary;
54
+ if (opts.pipe) {
55
+ if (process.stdin.isTTY) {
56
+ console.error("Error: --pipe requires piped input");
57
+ process.exit(1);
58
+ }
59
+ const stdinContent = (0, node_fs_1.readFileSync)("/dev/stdin", "utf8");
60
+ const lines = stdinContent.split("\n").filter((l) => l.trim() !== "");
61
+ summary = lines[lines.length - 1] ?? "";
62
+ }
63
+ else {
64
+ summary = summaryArg ?? "";
65
+ }
66
+ if (!summary.trim()) {
67
+ console.error("Error: summary is required");
68
+ process.exit(1);
69
+ }
70
+ // Build source object
71
+ const sourceObj = { type: sourceType };
72
+ // Compute traceback fingerprint for log/test sources with piped content
73
+ if (opts.pipe && (sourceType === "log" || sourceType === "test")) {
74
+ // summary was extracted from stdin; we want to fingerprint the full content
75
+ // re-read isn't possible; fingerprint from summary as best-effort
76
+ const fp = (0, fingerprint_js_1.tracebackFingerprint)(summary);
77
+ sourceObj["traceback_fingerprint"] = fp;
78
+ }
79
+ // Build file reference
80
+ let fileRefs;
81
+ if (opts.file) {
82
+ const fileRef = { path: opts.file };
83
+ if (opts.lines) {
84
+ const parts = opts.lines.split(",");
85
+ if (parts.length === 2) {
86
+ const start = parseInt(parts[0], 10);
87
+ const end = parseInt(parts[1], 10);
88
+ if (!isNaN(start) && !isNaN(end)) {
89
+ fileRef.lines = [start, end];
90
+ }
91
+ }
92
+ }
93
+ fileRefs = [fileRef];
94
+ }
95
+ // Parse tags
96
+ let tags;
97
+ if (opts.tags) {
98
+ tags = opts.tags
99
+ .split(",")
100
+ .map((t) => t.trim())
101
+ .filter((t) => t.length > 0);
102
+ }
103
+ const createdAt = new Date().toISOString();
104
+ const rawPayload = summary + (opts.file ?? "");
105
+ const id = (0, ticket_js_1.generateId)(sourceType, rawPayload, createdAt);
106
+ // Dedup check
107
+ try {
108
+ const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
109
+ for (const existing of openTickets) {
110
+ if (existing.summary === summary) {
111
+ console.error(`Warning: possible duplicate of ${existing.id}`);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ // ignore listing errors
118
+ }
119
+ // Handle parent
120
+ if (opts.parent) {
121
+ const { readTicketByPrefix } = require("../ticket.js");
122
+ try {
123
+ const parentTicket = readTicketByPrefix(repoRoot, opts.parent);
124
+ if (!parentTicket.relationships)
125
+ parentTicket.relationships = {};
126
+ if (!parentTicket.relationships.children)
127
+ parentTicket.relationships.children = [];
128
+ parentTicket.relationships.children.push(id);
129
+ parentTicket.updated_at = createdAt;
130
+ (0, ticket_js_1.writeTicket)(repoRoot, parentTicket);
131
+ }
132
+ catch {
133
+ console.error(`Error: parent ticket '${opts.parent}' not found`);
134
+ process.exit(1);
135
+ }
136
+ }
137
+ // Build ticket
138
+ const ticket = {
139
+ id,
140
+ type: ticketType,
141
+ state: "open",
142
+ summary,
143
+ source: sourceObj,
144
+ created_at: createdAt,
145
+ updated_at: createdAt,
146
+ };
147
+ if (fileRefs)
148
+ ticket.files = fileRefs;
149
+ if (tags && tags.length > 0)
150
+ ticket.tags = tags;
151
+ if (opts.parent) {
152
+ ticket.relationships = { parent: opts.parent };
153
+ }
154
+ (0, ticket_js_1.writeTicket)(repoRoot, ticket);
155
+ console.log(ticket.id);
156
+ });
157
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerScan(program: Command): void;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerScan = registerScan;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const config_js_1 = require("../config.js");
6
+ const context_js_1 = require("../context.js");
7
+ const errors_js_1 = require("../errors.js");
8
+ const git_js_1 = require("../git.js");
9
+ const scan_js_1 = require("../scan.js");
10
+ const ticket_js_1 = require("../ticket.js");
11
+ const VALID_TYPES = [
12
+ "bug",
13
+ "feature",
14
+ "refactor",
15
+ "chore",
16
+ "debt",
17
+ ];
18
+ function registerScan(program) {
19
+ program
20
+ .command("scan")
21
+ .description("Scan source tree for TODO/FIXME/etc comments and create tickets")
22
+ .option("--dry-run", "print what would be created, do not write")
23
+ .option("--type <type>", "ticket type for created tickets", "chore")
24
+ .action((opts) => {
25
+ try {
26
+ const ticketType = opts.type;
27
+ if (!VALID_TYPES.includes(ticketType)) {
28
+ console.error(`Error: invalid type '${ticketType}'. Must be one of: ${VALID_TYPES.join(", ")}`);
29
+ process.exit(1);
30
+ }
31
+ const ctx = (0, context_js_1.getContext)(true);
32
+ const { repoRoot, config } = ctx;
33
+ const scanPatterns = config.intake?.scan_patterns ?? [
34
+ "TODO",
35
+ "FIXME",
36
+ "HACK",
37
+ "XXX",
38
+ ];
39
+ const scanExclude = config.intake?.scan_exclude ?? [
40
+ ".todo",
41
+ "node_modules",
42
+ ".git",
43
+ "dist",
44
+ "build",
45
+ ];
46
+ (0, config_js_1.ensureTodoDir)(repoRoot);
47
+ const matches = (0, scan_js_1.scanComments)(repoRoot, scanPatterns, scanExclude);
48
+ // Load all open tickets for dedup
49
+ const openTickets = (0, ticket_js_1.listTickets)(repoRoot, "open");
50
+ let created = 0;
51
+ let skipped = 0;
52
+ for (const match of matches) {
53
+ const normalizedText = match.text.trim().toLowerCase();
54
+ const fingerprint = (0, node_crypto_1.createHash)("sha256")
55
+ .update(normalizedText)
56
+ .digest("hex");
57
+ // Check for existing ticket with same fingerprint
58
+ const exists = openTickets.some((t) => {
59
+ if (t.source.type === "comment" && t.source.raw) {
60
+ const existingFp = (0, node_crypto_1.createHash)("sha256")
61
+ .update(t.source.raw.trim().toLowerCase())
62
+ .digest("hex");
63
+ return existingFp === fingerprint;
64
+ }
65
+ return false;
66
+ });
67
+ if (exists) {
68
+ console.log(`Skipping existing: ${match.text}`);
69
+ skipped++;
70
+ continue;
71
+ }
72
+ if (opts.dryRun) {
73
+ console.log(`Would create: ${match.file}:${match.line} [${match.keyword}] ${match.text}`);
74
+ created++;
75
+ continue;
76
+ }
77
+ const createdAt = new Date().toISOString();
78
+ const id = (0, ticket_js_1.generateId)("comment", match.text + match.file + match.line, createdAt);
79
+ let lastCommit;
80
+ try {
81
+ const c = (0, git_js_1.getLastCommitForFile)(match.file, repoRoot);
82
+ if (c)
83
+ lastCommit = c;
84
+ }
85
+ catch {
86
+ lastCommit = undefined;
87
+ }
88
+ const ticket = {
89
+ id,
90
+ type: ticketType,
91
+ state: "open",
92
+ summary: `${match.keyword}: ${match.text}`.slice(0, 120),
93
+ source: { type: "comment", raw: match.text },
94
+ files: [
95
+ {
96
+ path: match.file,
97
+ lines: [match.line, match.line],
98
+ commit: lastCommit,
99
+ },
100
+ ],
101
+ created_at: createdAt,
102
+ updated_at: createdAt,
103
+ };
104
+ (0, ticket_js_1.writeTicket)(repoRoot, ticket);
105
+ openTickets.push(ticket); // add to in-memory list for subsequent dedup checks
106
+ created++;
107
+ }
108
+ console.log(`Created ${created} tickets, skipped ${skipped} duplicates`);
109
+ }
110
+ catch (err) {
111
+ (0, errors_js_1.handleError)(err);
112
+ }
113
+ });
114
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerShow(program: Command): void;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerShow = registerShow;
4
+ const context_js_1 = require("../context.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const ticket_js_1 = require("../ticket.js");
7
+ function formatDate(iso) {
8
+ return iso.slice(0, 10); // YYYY-MM-DD
9
+ }
10
+ function formatTicketDetail(ticket) {
11
+ const lines = [];
12
+ lines.push(`=== ${ticket.id} [${ticket.type}] ${ticket.state} ===`);
13
+ lines.push(`SUMMARY: ${ticket.summary}`);
14
+ if (ticket.tags && ticket.tags.length > 0) {
15
+ lines.push(`TAGS: ${ticket.tags.join(", ")}`);
16
+ }
17
+ lines.push(`CREATED: ${ticket.created_at}`);
18
+ lines.push(`UPDATED: ${ticket.updated_at}`);
19
+ if (ticket.description) {
20
+ lines.push("");
21
+ lines.push(`DESCRIPTION: ${ticket.description}`);
22
+ }
23
+ // Source
24
+ lines.push("");
25
+ lines.push(`SOURCE: ${ticket.source.type}`);
26
+ if ("test_file" in ticket.source && ticket.source.test_file) {
27
+ const fn = "test_function" in ticket.source
28
+ ? ticket.source.test_function
29
+ : undefined;
30
+ lines.push(` File: ${ticket.source.test_file}${fn ? `::${fn}` : ""}`);
31
+ }
32
+ if ("raw" in ticket.source && ticket.source.raw) {
33
+ lines.push(` Raw: ${ticket.source.raw}`);
34
+ }
35
+ // Files
36
+ if (ticket.files && ticket.files.length > 0) {
37
+ lines.push("");
38
+ lines.push("FILES:");
39
+ for (const f of ticket.files) {
40
+ let desc = ` ${f.path}`;
41
+ if (f.lines)
42
+ desc += `:${f.lines[0]}-${f.lines[1]}`;
43
+ if (f.commit)
44
+ desc += ` (${f.commit.slice(0, 7)})`;
45
+ if (f.note)
46
+ desc += ` — ${f.note}`;
47
+ lines.push(desc);
48
+ }
49
+ }
50
+ // Analysis
51
+ if (ticket.analysis && ticket.analysis.length > 0) {
52
+ lines.push("");
53
+ lines.push(`ANALYSIS: (${ticket.analysis.length} entries)`);
54
+ for (let i = 0; i < ticket.analysis.length; i++) {
55
+ const a = ticket.analysis[i];
56
+ const conf = a.confidence ? ` [${a.confidence}]` : "";
57
+ lines.push(` [${i}] ${formatDate(a.timestamp)} • ${a.type}${conf}`);
58
+ lines.push(` ${a.content}`);
59
+ }
60
+ }
61
+ // Relationships
62
+ if (ticket.relationships) {
63
+ const rel = ticket.relationships;
64
+ const parts = [];
65
+ if (rel.parent)
66
+ parts.push(` parent: ${rel.parent}`);
67
+ if (rel.children && rel.children.length > 0)
68
+ parts.push(` children: ${rel.children.join(", ")}`);
69
+ if (rel.depends_on && rel.depends_on.length > 0)
70
+ parts.push(` depends_on: ${rel.depends_on.join(", ")}`);
71
+ if (rel.blocks && rel.blocks.length > 0)
72
+ parts.push(` blocks: ${rel.blocks.join(", ")}`);
73
+ if (rel.related && rel.related.length > 0)
74
+ parts.push(` related: ${rel.related.join(", ")}`);
75
+ if (rel.duplicates)
76
+ parts.push(` duplicates: ${rel.duplicates}`);
77
+ if (rel.linked_commits && rel.linked_commits.length > 0)
78
+ parts.push(` linked_commits: ${rel.linked_commits.join(", ")}`);
79
+ if (parts.length > 0) {
80
+ lines.push("");
81
+ lines.push("RELATIONSHIPS:");
82
+ lines.push(...parts);
83
+ }
84
+ }
85
+ // Work
86
+ if (ticket.work) {
87
+ lines.push("");
88
+ lines.push("WORK:");
89
+ lines.push(` branch: ${ticket.work.branch}`);
90
+ lines.push(` base: ${ticket.work.base_branch}`);
91
+ lines.push(` started: ${ticket.work.started_at} by ${ticket.work.started_by}`);
92
+ }
93
+ // Resolution
94
+ if (ticket.resolution) {
95
+ lines.push("");
96
+ lines.push("RESOLUTION:");
97
+ lines.push(` commit: ${ticket.resolution.commit}`);
98
+ lines.push(` resolved_at: ${ticket.resolution.resolved_at} by ${ticket.resolution.resolved_by}`);
99
+ if (ticket.resolution.test_file) {
100
+ const fn = ticket.resolution.test_function
101
+ ? `::${ticket.resolution.test_function}`
102
+ : "";
103
+ lines.push(` test: ${ticket.resolution.test_file}${fn}`);
104
+ }
105
+ if (ticket.resolution.note) {
106
+ lines.push(` note: ${ticket.resolution.note}`);
107
+ }
108
+ }
109
+ return lines.join("\n");
110
+ }
111
+ function registerShow(program) {
112
+ program
113
+ .command("show <id>")
114
+ .description("Show ticket details")
115
+ .option("--raw", "dump raw JSON")
116
+ .action((id, opts) => {
117
+ const ctx = (0, context_js_1.getContext)(true);
118
+ const { repoRoot } = ctx;
119
+ try {
120
+ const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
121
+ if (opts.raw) {
122
+ console.log(JSON.stringify(ticket, null, 2));
123
+ }
124
+ else {
125
+ console.log(formatTicketDetail(ticket));
126
+ }
127
+ }
128
+ catch (err) {
129
+ (0, errors_js_1.handleError)(err);
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerTransition(program: Command): void;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTransition = registerTransition;
4
+ const context_js_1 = require("../context.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const state_js_1 = require("../state.js");
7
+ const ticket_js_1 = require("../ticket.js");
8
+ const VALID_STATES = [
9
+ "open",
10
+ "active",
11
+ "blocked",
12
+ "done",
13
+ "wontfix",
14
+ "duplicate",
15
+ ];
16
+ function registerTransition(program) {
17
+ program
18
+ .command("transition <id> <state>")
19
+ .description("Transition ticket state")
20
+ .option("--commit <sha>", "resolution commit")
21
+ .option("--test <file::func>", "test file and function (colon-separated)")
22
+ .option("--note <text>", "resolution note")
23
+ .option("--depends-on <id>", "set dependency")
24
+ .option("--duplicate-of <id>", "mark as duplicate")
25
+ .action((id, state, opts) => {
26
+ const ctx = (0, context_js_1.getContext)(true);
27
+ const { repoRoot } = ctx;
28
+ if (!VALID_STATES.includes(state)) {
29
+ console.error(`Error: invalid state '${state}'. Must be one of: ${VALID_STATES.join(", ")}`);
30
+ process.exit(1);
31
+ }
32
+ try {
33
+ const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
34
+ const fromState = ticket.state;
35
+ // Parse --test as "file::func" or "file"
36
+ let testFile;
37
+ let testFunction;
38
+ if (opts.test) {
39
+ const parts = opts.test.split("::");
40
+ testFile = parts[0];
41
+ testFunction = parts[1];
42
+ }
43
+ const params = {
44
+ commit: opts.commit,
45
+ test_file: testFile,
46
+ test_function: testFunction,
47
+ note: opts.note,
48
+ depends_on: opts.dependsOn,
49
+ duplicate_of: opts.duplicateOf,
50
+ };
51
+ try {
52
+ (0, state_js_1.validateTransition)(ticket, state, params, repoRoot);
53
+ }
54
+ catch (err) {
55
+ console.error(`Error: ${err.message}`);
56
+ process.exit(1);
57
+ }
58
+ const updated = (0, state_js_1.applyTransition)(ticket, state, params, repoRoot);
59
+ (0, ticket_js_1.writeTicket)(repoRoot, updated);
60
+ console.log(`Transitioned ${updated.id}: ${fromState} → ${state}`);
61
+ }
62
+ catch (err) {
63
+ (0, errors_js_1.handleError)(err);
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerWork(program: Command): void;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerWork = registerWork;
4
+ const context_js_1 = require("../context.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ const git_js_1 = require("../git.js");
7
+ const state_js_1 = require("../state.js");
8
+ const ticket_js_1 = require("../ticket.js");
9
+ function registerWork(program) {
10
+ program
11
+ .command("work <id>")
12
+ .description("Start or resume work on a ticket")
13
+ .option("--branch <name>", "override branch name")
14
+ .option("--actor <name>", "override actor (also reads TODO_ACTOR env)")
15
+ .action((id, opts) => {
16
+ const ctx = (0, context_js_1.getContext)(true);
17
+ const { repoRoot } = ctx;
18
+ try {
19
+ const ticket = (0, ticket_js_1.readTicketByPrefix)(repoRoot, id);
20
+ // Check not terminal
21
+ if (ticket_js_1.TERMINAL_STATES.includes(ticket.state)) {
22
+ console.error(`Error: ticket ${ticket.id} is in terminal state '${ticket.state}'. Cannot work on it.`);
23
+ process.exit(1);
24
+ }
25
+ // Resolve branch name
26
+ let branch;
27
+ if (opts.branch) {
28
+ branch = opts.branch;
29
+ }
30
+ else if (ticket.relationships?.parent) {
31
+ branch = `todo/${ticket.relationships.parent}`;
32
+ }
33
+ else {
34
+ branch = `todo/${ticket.id}`;
35
+ }
36
+ // Resolve actor
37
+ let actor;
38
+ if (opts.actor) {
39
+ actor = opts.actor;
40
+ }
41
+ else if (process.env["TODO_ACTOR"]) {
42
+ actor = process.env["TODO_ACTOR"];
43
+ }
44
+ else {
45
+ try {
46
+ actor = (0, git_js_1.getGitUserName)(repoRoot);
47
+ }
48
+ catch {
49
+ actor = "unknown";
50
+ }
51
+ }
52
+ const defaultBranch = (0, git_js_1.getDefaultBranch)(repoRoot);
53
+ if ((0, git_js_1.branchExists)(branch, repoRoot)) {
54
+ // Resume
55
+ (0, git_js_1.checkoutBranch)(branch, repoRoot);
56
+ // Ensure ticket is active
57
+ if (ticket.state !== "active") {
58
+ const params = { actor };
59
+ try {
60
+ const updated = (0, state_js_1.applyTransition)(ticket, "active", params, repoRoot);
61
+ (0, ticket_js_1.writeTicket)(repoRoot, updated);
62
+ }
63
+ catch {
64
+ // if transition fails (e.g. already done), just proceed
65
+ }
66
+ }
67
+ let ahead;
68
+ try {
69
+ ahead = (0, git_js_1.getCommitsAhead)(branch, defaultBranch, repoRoot);
70
+ }
71
+ catch {
72
+ ahead = 0;
73
+ }
74
+ console.log(`Resumed branch ${branch} — ticket ${ticket.id} is active. Branch has ${ahead} commits ahead of ${defaultBranch}.`);
75
+ }
76
+ else {
77
+ // New branch
78
+ const now = new Date().toISOString();
79
+ const params = {
80
+ actor,
81
+ };
82
+ let updated;
83
+ try {
84
+ updated = (0, state_js_1.applyTransition)(ticket, "active", params, repoRoot);
85
+ }
86
+ catch (err) {
87
+ console.error(`Error: ${err.message}`);
88
+ process.exit(1);
89
+ }
90
+ // Populate work block
91
+ updated.work = {
92
+ branch,
93
+ base_branch: defaultBranch,
94
+ started_at: now,
95
+ started_by: actor,
96
+ };
97
+ updated.updated_at = now;
98
+ (0, ticket_js_1.writeTicket)(repoRoot, updated);
99
+ (0, git_js_1.createBranch)(branch, repoRoot);
100
+ console.log(`Created branch ${branch} — ticket ${ticket.id} is now active.`);
101
+ }
102
+ // Check depends_on: warn if dep commit not ancestor of HEAD
103
+ const deps = ticket.relationships?.depends_on ?? [];
104
+ for (const depId of deps) {
105
+ try {
106
+ const dep = (0, ticket_js_1.readTicket)(repoRoot, depId);
107
+ if (dep.resolution?.commit) {
108
+ const depCommit = dep.resolution.commit;
109
+ try {
110
+ if (!(0, git_js_1.isAncestor)(depCommit, "HEAD", repoRoot)) {
111
+ console.error(`Warning: dependency ${depId} resolved at ${depCommit} is not an ancestor of HEAD`);
112
+ }
113
+ }
114
+ catch {
115
+ // ignore ancestor check failures
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // ignore dep lookup failures
121
+ }
122
+ }
123
+ }
124
+ catch (err) {
125
+ (0, errors_js_1.handleError)(err);
126
+ }
127
+ });
128
+ }
@@ -0,0 +1,7 @@
1
+ import type { Config } from "./types.js";
2
+ export declare const DEFAULT_CONFIG: Config;
3
+ export declare function loadConfig(repoRoot: string): Config;
4
+ export declare function getTodoDir(repoRoot: string): string;
5
+ export declare function ensureTodoDir(repoRoot: string): void;
6
+ export declare function getIdLength(config: Config): number;
7
+ export declare function getCommitPrefix(config: Config): string;