@anthropologies/claudestory 0.1.11 → 0.1.13
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/dist/cli.js +1872 -143
- package/dist/index.d.ts +42 -34
- package/dist/index.js +22 -4
- package/dist/mcp.js +2865 -1203
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -1,443 +1,449 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// src/core/project-root-discovery.ts
|
|
11
|
-
import { existsSync, accessSync, constants } from "fs";
|
|
12
|
-
import { resolve, dirname, join } from "path";
|
|
13
|
-
|
|
14
|
-
// src/core/errors.ts
|
|
15
|
-
var CURRENT_SCHEMA_VERSION = 1;
|
|
16
|
-
var ProjectLoaderError = class extends Error {
|
|
17
|
-
constructor(code, message, cause) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.code = code;
|
|
20
|
-
this.cause = cause;
|
|
21
|
-
}
|
|
22
|
-
name = "ProjectLoaderError";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
10
|
};
|
|
24
|
-
var INTEGRITY_WARNING_TYPES = [
|
|
25
|
-
"parse_error",
|
|
26
|
-
"schema_error",
|
|
27
|
-
"duplicate_id"
|
|
28
|
-
];
|
|
29
11
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
var
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (envRoot) {
|
|
37
|
-
const resolved = resolve(envRoot);
|
|
38
|
-
return checkRoot(resolved);
|
|
39
|
-
}
|
|
40
|
-
let current = resolve(startDir ?? process.cwd());
|
|
41
|
-
for (; ; ) {
|
|
42
|
-
const result = checkRoot(current);
|
|
43
|
-
if (result) return result;
|
|
44
|
-
const parent = dirname(current);
|
|
45
|
-
if (parent === current) break;
|
|
46
|
-
current = parent;
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
function checkRoot(candidate) {
|
|
51
|
-
if (existsSync(join(candidate, CONFIG_PATH))) {
|
|
52
|
-
return candidate;
|
|
53
|
-
}
|
|
54
|
-
if (existsSync(join(candidate, STORY_DIR))) {
|
|
55
|
-
try {
|
|
56
|
-
accessSync(join(candidate, STORY_DIR), constants.R_OK);
|
|
57
|
-
} catch {
|
|
58
|
-
throw new ProjectLoaderError(
|
|
59
|
-
"io_error",
|
|
60
|
-
`Permission denied: cannot read .story/ in ${candidate}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
63
18
|
}
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// src/mcp/tools.ts
|
|
68
|
-
import { z as z8 } from "zod";
|
|
69
|
-
import { join as join7 } from "path";
|
|
70
|
-
|
|
71
|
-
// src/core/project-loader.ts
|
|
72
|
-
import {
|
|
73
|
-
readdir as readdir2,
|
|
74
|
-
readFile as readFile2,
|
|
75
|
-
writeFile,
|
|
76
|
-
rename,
|
|
77
|
-
unlink,
|
|
78
|
-
stat,
|
|
79
|
-
realpath,
|
|
80
|
-
lstat,
|
|
81
|
-
open,
|
|
82
|
-
mkdir
|
|
83
|
-
} from "fs/promises";
|
|
84
|
-
import { existsSync as existsSync3 } from "fs";
|
|
85
|
-
import { join as join3, resolve as resolve2, relative as relative2, extname as extname2, dirname as dirname2, basename } from "path";
|
|
86
|
-
import lockfile from "proper-lockfile";
|
|
19
|
+
});
|
|
87
20
|
|
|
88
|
-
// src/
|
|
89
|
-
|
|
21
|
+
// src/core/errors.ts
|
|
22
|
+
var CURRENT_SCHEMA_VERSION, ProjectLoaderError, INTEGRITY_WARNING_TYPES;
|
|
23
|
+
var init_errors = __esm({
|
|
24
|
+
"src/core/errors.ts"() {
|
|
25
|
+
"use strict";
|
|
26
|
+
init_esm_shims();
|
|
27
|
+
CURRENT_SCHEMA_VERSION = 1;
|
|
28
|
+
ProjectLoaderError = class extends Error {
|
|
29
|
+
constructor(code, message, cause) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.cause = cause;
|
|
33
|
+
}
|
|
34
|
+
name = "ProjectLoaderError";
|
|
35
|
+
};
|
|
36
|
+
INTEGRITY_WARNING_TYPES = [
|
|
37
|
+
"parse_error",
|
|
38
|
+
"schema_error",
|
|
39
|
+
"duplicate_id"
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
90
43
|
|
|
91
44
|
// src/models/types.ts
|
|
92
45
|
import { z } from "zod";
|
|
93
|
-
var TICKET_ID_REGEX
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
|
|
46
|
+
var TICKET_ID_REGEX, ISSUE_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, ISSUE_STATUSES, ISSUE_SEVERITIES, NOTE_STATUSES, NOTE_ID_REGEX, NoteIdSchema, DATE_REGEX, DateSchema, TicketIdSchema, IssueIdSchema;
|
|
47
|
+
var init_types = __esm({
|
|
48
|
+
"src/models/types.ts"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
init_esm_shims();
|
|
51
|
+
TICKET_ID_REGEX = /^T-\d+[a-z]?$/;
|
|
52
|
+
ISSUE_ID_REGEX = /^ISS-\d+$/;
|
|
53
|
+
TICKET_STATUSES = ["open", "inprogress", "complete"];
|
|
54
|
+
TICKET_TYPES = ["task", "feature", "chore"];
|
|
55
|
+
ISSUE_STATUSES = ["open", "inprogress", "resolved"];
|
|
56
|
+
ISSUE_SEVERITIES = ["critical", "high", "medium", "low"];
|
|
57
|
+
NOTE_STATUSES = ["active", "archived"];
|
|
58
|
+
NOTE_ID_REGEX = /^N-\d+$/;
|
|
59
|
+
NoteIdSchema = z.string().regex(NOTE_ID_REGEX, "Note ID must match N-NNN");
|
|
60
|
+
DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
61
|
+
DateSchema = z.string().regex(DATE_REGEX, "Date must be YYYY-MM-DD").refine(
|
|
62
|
+
(val) => {
|
|
63
|
+
const d = /* @__PURE__ */ new Date(val + "T00:00:00Z");
|
|
64
|
+
return !isNaN(d.getTime()) && d.toISOString().startsWith(val);
|
|
65
|
+
},
|
|
66
|
+
{ message: "Invalid calendar date" }
|
|
67
|
+
);
|
|
68
|
+
TicketIdSchema = z.string().regex(TICKET_ID_REGEX, "Ticket ID must match T-NNN or T-NNNx");
|
|
69
|
+
IssueIdSchema = z.string().regex(ISSUE_ID_REGEX, "Issue ID must match ISS-NNN");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
112
72
|
|
|
113
73
|
// src/models/ticket.ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
74
|
+
import { z as z2 } from "zod";
|
|
75
|
+
var TicketSchema;
|
|
76
|
+
var init_ticket = __esm({
|
|
77
|
+
"src/models/ticket.ts"() {
|
|
78
|
+
"use strict";
|
|
79
|
+
init_esm_shims();
|
|
80
|
+
init_types();
|
|
81
|
+
TicketSchema = z2.object({
|
|
82
|
+
id: TicketIdSchema,
|
|
83
|
+
title: z2.string().min(1),
|
|
84
|
+
description: z2.string(),
|
|
85
|
+
type: z2.enum(TICKET_TYPES),
|
|
86
|
+
status: z2.enum(TICKET_STATUSES),
|
|
87
|
+
phase: z2.string().nullable(),
|
|
88
|
+
order: z2.number().int(),
|
|
89
|
+
createdDate: DateSchema,
|
|
90
|
+
completedDate: DateSchema.nullable(),
|
|
91
|
+
blockedBy: z2.array(TicketIdSchema),
|
|
92
|
+
parentTicket: TicketIdSchema.nullable().optional(),
|
|
93
|
+
// Attribution fields — unused in v1, baked in to avoid future migration
|
|
94
|
+
createdBy: z2.string().nullable().optional(),
|
|
95
|
+
assignedTo: z2.string().nullable().optional(),
|
|
96
|
+
lastModifiedBy: z2.string().nullable().optional()
|
|
97
|
+
}).passthrough();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
131
100
|
|
|
132
101
|
// src/models/issue.ts
|
|
133
102
|
import { z as z3 } from "zod";
|
|
134
|
-
var IssueSchema
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
103
|
+
var IssueSchema;
|
|
104
|
+
var init_issue = __esm({
|
|
105
|
+
"src/models/issue.ts"() {
|
|
106
|
+
"use strict";
|
|
107
|
+
init_esm_shims();
|
|
108
|
+
init_types();
|
|
109
|
+
IssueSchema = z3.object({
|
|
110
|
+
id: IssueIdSchema,
|
|
111
|
+
title: z3.string().min(1),
|
|
112
|
+
status: z3.enum(ISSUE_STATUSES),
|
|
113
|
+
severity: z3.enum(ISSUE_SEVERITIES),
|
|
114
|
+
components: z3.array(z3.string()),
|
|
115
|
+
impact: z3.string(),
|
|
116
|
+
resolution: z3.string().nullable(),
|
|
117
|
+
location: z3.array(z3.string()),
|
|
118
|
+
discoveredDate: DateSchema,
|
|
119
|
+
resolvedDate: DateSchema.nullable(),
|
|
120
|
+
relatedTickets: z3.array(TicketIdSchema),
|
|
121
|
+
// Optional fields — older issues may omit these
|
|
122
|
+
order: z3.number().int().optional(),
|
|
123
|
+
phase: z3.string().nullable().optional(),
|
|
124
|
+
// Attribution fields — unused in v1
|
|
125
|
+
createdBy: z3.string().nullable().optional(),
|
|
126
|
+
assignedTo: z3.string().nullable().optional(),
|
|
127
|
+
lastModifiedBy: z3.string().nullable().optional()
|
|
128
|
+
}).passthrough();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
154
131
|
|
|
155
132
|
// src/models/note.ts
|
|
156
133
|
import { z as z4 } from "zod";
|
|
157
|
-
var NoteSchema
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
134
|
+
var NoteSchema;
|
|
135
|
+
var init_note = __esm({
|
|
136
|
+
"src/models/note.ts"() {
|
|
137
|
+
"use strict";
|
|
138
|
+
init_esm_shims();
|
|
139
|
+
init_types();
|
|
140
|
+
NoteSchema = z4.object({
|
|
141
|
+
id: NoteIdSchema,
|
|
142
|
+
title: z4.string().nullable(),
|
|
143
|
+
content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
|
|
144
|
+
tags: z4.array(z4.string()),
|
|
145
|
+
status: z4.enum(NOTE_STATUSES),
|
|
146
|
+
createdDate: DateSchema,
|
|
147
|
+
updatedDate: DateSchema
|
|
148
|
+
}).passthrough();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
166
151
|
|
|
167
152
|
// src/models/roadmap.ts
|
|
168
153
|
import { z as z5 } from "zod";
|
|
169
|
-
var BlockerSchema
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}).passthrough();
|
|
154
|
+
var BlockerSchema, PhaseSchema, RoadmapSchema;
|
|
155
|
+
var init_roadmap = __esm({
|
|
156
|
+
"src/models/roadmap.ts"() {
|
|
157
|
+
"use strict";
|
|
158
|
+
init_esm_shims();
|
|
159
|
+
init_types();
|
|
160
|
+
BlockerSchema = z5.object({
|
|
161
|
+
name: z5.string().min(1),
|
|
162
|
+
// Legacy format (pre-T-082)
|
|
163
|
+
cleared: z5.boolean().optional(),
|
|
164
|
+
// New date-based format (T-082 migration)
|
|
165
|
+
createdDate: DateSchema.optional(),
|
|
166
|
+
clearedDate: DateSchema.nullable().optional(),
|
|
167
|
+
// Present in all current data but optional for future minimal blockers
|
|
168
|
+
note: z5.string().nullable().optional()
|
|
169
|
+
}).passthrough();
|
|
170
|
+
PhaseSchema = z5.object({
|
|
171
|
+
id: z5.string().min(1),
|
|
172
|
+
label: z5.string(),
|
|
173
|
+
name: z5.string(),
|
|
174
|
+
description: z5.string(),
|
|
175
|
+
summary: z5.string().optional()
|
|
176
|
+
}).passthrough();
|
|
177
|
+
RoadmapSchema = z5.object({
|
|
178
|
+
title: z5.string(),
|
|
179
|
+
date: DateSchema,
|
|
180
|
+
phases: z5.array(PhaseSchema),
|
|
181
|
+
blockers: z5.array(BlockerSchema)
|
|
182
|
+
}).passthrough();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
192
185
|
|
|
193
186
|
// src/models/config.ts
|
|
194
187
|
import { z as z6 } from "zod";
|
|
195
|
-
var FeaturesSchema
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
188
|
+
var FeaturesSchema, ConfigSchema;
|
|
189
|
+
var init_config = __esm({
|
|
190
|
+
"src/models/config.ts"() {
|
|
191
|
+
"use strict";
|
|
192
|
+
init_esm_shims();
|
|
193
|
+
FeaturesSchema = z6.object({
|
|
194
|
+
tickets: z6.boolean(),
|
|
195
|
+
issues: z6.boolean(),
|
|
196
|
+
handovers: z6.boolean(),
|
|
197
|
+
roadmap: z6.boolean(),
|
|
198
|
+
reviews: z6.boolean()
|
|
199
|
+
}).passthrough();
|
|
200
|
+
ConfigSchema = z6.object({
|
|
201
|
+
version: z6.number().int().min(1),
|
|
202
|
+
schemaVersion: z6.number().int().optional(),
|
|
203
|
+
project: z6.string().min(1),
|
|
204
|
+
type: z6.string(),
|
|
205
|
+
language: z6.string(),
|
|
206
|
+
features: FeaturesSchema,
|
|
207
|
+
recipe: z6.string().optional()
|
|
208
|
+
}).passthrough();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
210
211
|
|
|
211
212
|
// src/core/project-state.ts
|
|
212
|
-
var ProjectState
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
213
|
+
var ProjectState;
|
|
214
|
+
var init_project_state = __esm({
|
|
215
|
+
"src/core/project-state.ts"() {
|
|
216
|
+
"use strict";
|
|
217
|
+
init_esm_shims();
|
|
218
|
+
ProjectState = class _ProjectState {
|
|
219
|
+
// --- Public raw inputs (readonly) ---
|
|
220
|
+
tickets;
|
|
221
|
+
issues;
|
|
222
|
+
notes;
|
|
223
|
+
roadmap;
|
|
224
|
+
config;
|
|
225
|
+
handoverFilenames;
|
|
226
|
+
// --- Derived (public readonly) ---
|
|
227
|
+
umbrellaIDs;
|
|
228
|
+
leafTickets;
|
|
229
|
+
leafTicketCount;
|
|
230
|
+
completeLeafTicketCount;
|
|
231
|
+
// --- Derived (private) ---
|
|
232
|
+
leafTicketsByPhase;
|
|
233
|
+
childrenByParent;
|
|
234
|
+
reverseBlocksMap;
|
|
235
|
+
ticketsByID;
|
|
236
|
+
issuesByID;
|
|
237
|
+
notesByID;
|
|
238
|
+
// --- Counts ---
|
|
239
|
+
totalTicketCount;
|
|
240
|
+
openTicketCount;
|
|
241
|
+
completeTicketCount;
|
|
242
|
+
openIssueCount;
|
|
243
|
+
issuesBySeverity;
|
|
244
|
+
activeNoteCount;
|
|
245
|
+
archivedNoteCount;
|
|
246
|
+
constructor(input) {
|
|
247
|
+
this.tickets = input.tickets;
|
|
248
|
+
this.issues = input.issues;
|
|
249
|
+
this.notes = input.notes;
|
|
250
|
+
this.roadmap = input.roadmap;
|
|
251
|
+
this.config = input.config;
|
|
252
|
+
this.handoverFilenames = input.handoverFilenames;
|
|
253
|
+
const parentIDs = /* @__PURE__ */ new Set();
|
|
254
|
+
for (const t of input.tickets) {
|
|
255
|
+
if (t.parentTicket != null) {
|
|
256
|
+
parentIDs.add(t.parentTicket);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this.umbrellaIDs = parentIDs;
|
|
260
|
+
this.leafTickets = input.tickets.filter((t) => !parentIDs.has(t.id));
|
|
261
|
+
this.leafTicketCount = this.leafTickets.length;
|
|
262
|
+
this.completeLeafTicketCount = this.leafTickets.filter(
|
|
263
|
+
(t) => t.status === "complete"
|
|
264
|
+
).length;
|
|
265
|
+
const byPhase = /* @__PURE__ */ new Map();
|
|
266
|
+
for (const t of this.leafTickets) {
|
|
267
|
+
const phase = t.phase;
|
|
268
|
+
const arr = byPhase.get(phase);
|
|
269
|
+
if (arr) {
|
|
270
|
+
arr.push(t);
|
|
271
|
+
} else {
|
|
272
|
+
byPhase.set(phase, [t]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const [, arr] of byPhase) {
|
|
276
|
+
arr.sort((a, b) => a.order - b.order);
|
|
277
|
+
}
|
|
278
|
+
this.leafTicketsByPhase = byPhase;
|
|
279
|
+
const children = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const t of input.tickets) {
|
|
281
|
+
if (t.parentTicket != null) {
|
|
282
|
+
const arr = children.get(t.parentTicket);
|
|
283
|
+
if (arr) {
|
|
284
|
+
arr.push(t);
|
|
285
|
+
} else {
|
|
286
|
+
children.set(t.parentTicket, [t]);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.childrenByParent = children;
|
|
291
|
+
const reverseBlocks = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const t of input.tickets) {
|
|
293
|
+
for (const blockerID of t.blockedBy) {
|
|
294
|
+
const arr = reverseBlocks.get(blockerID);
|
|
295
|
+
if (arr) {
|
|
296
|
+
arr.push(t);
|
|
297
|
+
} else {
|
|
298
|
+
reverseBlocks.set(blockerID, [t]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.reverseBlocksMap = reverseBlocks;
|
|
303
|
+
const tByID = /* @__PURE__ */ new Map();
|
|
304
|
+
for (const t of input.tickets) {
|
|
305
|
+
if (!tByID.has(t.id)) {
|
|
306
|
+
tByID.set(t.id, t);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this.ticketsByID = tByID;
|
|
310
|
+
const iByID = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const i of input.issues) {
|
|
312
|
+
iByID.set(i.id, i);
|
|
313
|
+
}
|
|
314
|
+
this.issuesByID = iByID;
|
|
315
|
+
const nByID = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const n of input.notes) {
|
|
317
|
+
nByID.set(n.id, n);
|
|
318
|
+
}
|
|
319
|
+
this.notesByID = nByID;
|
|
320
|
+
this.totalTicketCount = input.tickets.length;
|
|
321
|
+
this.openTicketCount = input.tickets.filter(
|
|
322
|
+
(t) => t.status !== "complete"
|
|
323
|
+
).length;
|
|
324
|
+
this.completeTicketCount = input.tickets.filter(
|
|
325
|
+
(t) => t.status === "complete"
|
|
326
|
+
).length;
|
|
327
|
+
this.openIssueCount = input.issues.filter(
|
|
328
|
+
(i) => i.status === "open"
|
|
329
|
+
).length;
|
|
330
|
+
const bySev = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const i of input.issues) {
|
|
332
|
+
if (i.status === "open") {
|
|
333
|
+
bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.issuesBySeverity = bySev;
|
|
337
|
+
this.activeNoteCount = input.notes.filter(
|
|
338
|
+
(n) => n.status === "active"
|
|
339
|
+
).length;
|
|
340
|
+
this.archivedNoteCount = input.notes.filter(
|
|
341
|
+
(n) => n.status === "archived"
|
|
342
|
+
).length;
|
|
251
343
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.leafTicketCount = this.leafTickets.length;
|
|
256
|
-
this.completeLeafTicketCount = this.leafTickets.filter(
|
|
257
|
-
(t) => t.status === "complete"
|
|
258
|
-
).length;
|
|
259
|
-
const byPhase = /* @__PURE__ */ new Map();
|
|
260
|
-
for (const t of this.leafTickets) {
|
|
261
|
-
const phase = t.phase;
|
|
262
|
-
const arr = byPhase.get(phase);
|
|
263
|
-
if (arr) {
|
|
264
|
-
arr.push(t);
|
|
265
|
-
} else {
|
|
266
|
-
byPhase.set(phase, [t]);
|
|
344
|
+
// --- Query Methods ---
|
|
345
|
+
isUmbrella(ticket) {
|
|
346
|
+
return this.umbrellaIDs.has(ticket.id);
|
|
267
347
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
arr.sort((a, b) => a.order - b.order);
|
|
271
|
-
}
|
|
272
|
-
this.leafTicketsByPhase = byPhase;
|
|
273
|
-
const children = /* @__PURE__ */ new Map();
|
|
274
|
-
for (const t of input.tickets) {
|
|
275
|
-
if (t.parentTicket != null) {
|
|
276
|
-
const arr = children.get(t.parentTicket);
|
|
277
|
-
if (arr) {
|
|
278
|
-
arr.push(t);
|
|
279
|
-
} else {
|
|
280
|
-
children.set(t.parentTicket, [t]);
|
|
281
|
-
}
|
|
348
|
+
phaseTickets(phaseId) {
|
|
349
|
+
return this.leafTicketsByPhase.get(phaseId) ?? [];
|
|
282
350
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
for (const blockerID of t.blockedBy) {
|
|
288
|
-
const arr = reverseBlocks.get(blockerID);
|
|
289
|
-
if (arr) {
|
|
290
|
-
arr.push(t);
|
|
291
|
-
} else {
|
|
292
|
-
reverseBlocks.set(blockerID, [t]);
|
|
293
|
-
}
|
|
351
|
+
/** Phase status derived from leaf tickets only. Umbrella stored status is ignored. */
|
|
352
|
+
phaseStatus(phaseId) {
|
|
353
|
+
const leaves = this.phaseTickets(phaseId);
|
|
354
|
+
return _ProjectState.aggregateStatus(leaves);
|
|
294
355
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const tByID = /* @__PURE__ */ new Map();
|
|
298
|
-
for (const t of input.tickets) {
|
|
299
|
-
if (!tByID.has(t.id)) {
|
|
300
|
-
tByID.set(t.id, t);
|
|
356
|
+
umbrellaChildren(ticketId) {
|
|
357
|
+
return this.childrenByParent.get(ticketId) ?? [];
|
|
301
358
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
this.issuesByID = iByID;
|
|
309
|
-
const nByID = /* @__PURE__ */ new Map();
|
|
310
|
-
for (const n of input.notes) {
|
|
311
|
-
nByID.set(n.id, n);
|
|
312
|
-
}
|
|
313
|
-
this.notesByID = nByID;
|
|
314
|
-
this.totalTicketCount = input.tickets.length;
|
|
315
|
-
this.openTicketCount = input.tickets.filter(
|
|
316
|
-
(t) => t.status !== "complete"
|
|
317
|
-
).length;
|
|
318
|
-
this.completeTicketCount = input.tickets.filter(
|
|
319
|
-
(t) => t.status === "complete"
|
|
320
|
-
).length;
|
|
321
|
-
this.openIssueCount = input.issues.filter(
|
|
322
|
-
(i) => i.status === "open"
|
|
323
|
-
).length;
|
|
324
|
-
const bySev = /* @__PURE__ */ new Map();
|
|
325
|
-
for (const i of input.issues) {
|
|
326
|
-
if (i.status === "open") {
|
|
327
|
-
bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
|
|
359
|
+
/** Umbrella status derived from descendant leaf tickets (recursive traversal). */
|
|
360
|
+
umbrellaStatus(ticketId) {
|
|
361
|
+
const visited = /* @__PURE__ */ new Set();
|
|
362
|
+
const leaves = this.descendantLeaves(ticketId, visited);
|
|
363
|
+
return _ProjectState.aggregateStatus(leaves);
|
|
328
364
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.activeNoteCount = input.notes.filter(
|
|
332
|
-
(n) => n.status === "active"
|
|
333
|
-
).length;
|
|
334
|
-
this.archivedNoteCount = input.notes.filter(
|
|
335
|
-
(n) => n.status === "archived"
|
|
336
|
-
).length;
|
|
337
|
-
}
|
|
338
|
-
// --- Query Methods ---
|
|
339
|
-
isUmbrella(ticket) {
|
|
340
|
-
return this.umbrellaIDs.has(ticket.id);
|
|
341
|
-
}
|
|
342
|
-
phaseTickets(phaseId) {
|
|
343
|
-
return this.leafTicketsByPhase.get(phaseId) ?? [];
|
|
344
|
-
}
|
|
345
|
-
/** Phase status derived from leaf tickets only. Umbrella stored status is ignored. */
|
|
346
|
-
phaseStatus(phaseId) {
|
|
347
|
-
const leaves = this.phaseTickets(phaseId);
|
|
348
|
-
return _ProjectState.aggregateStatus(leaves);
|
|
349
|
-
}
|
|
350
|
-
umbrellaChildren(ticketId) {
|
|
351
|
-
return this.childrenByParent.get(ticketId) ?? [];
|
|
352
|
-
}
|
|
353
|
-
/** Umbrella status derived from descendant leaf tickets (recursive traversal). */
|
|
354
|
-
umbrellaStatus(ticketId) {
|
|
355
|
-
const visited = /* @__PURE__ */ new Set();
|
|
356
|
-
const leaves = this.descendantLeaves(ticketId, visited);
|
|
357
|
-
return _ProjectState.aggregateStatus(leaves);
|
|
358
|
-
}
|
|
359
|
-
reverseBlocks(ticketId) {
|
|
360
|
-
return this.reverseBlocksMap.get(ticketId) ?? [];
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* A ticket is blocked if any blockedBy reference points to a non-complete ticket.
|
|
364
|
-
* Unknown blocker IDs treated as blocked (conservative — unknown dependency = assume not cleared).
|
|
365
|
-
*/
|
|
366
|
-
isBlocked(ticket) {
|
|
367
|
-
if (ticket.blockedBy.length === 0) return false;
|
|
368
|
-
return ticket.blockedBy.some((blockerID) => {
|
|
369
|
-
const blocker = this.ticketsByID.get(blockerID);
|
|
370
|
-
if (!blocker) return true;
|
|
371
|
-
return blocker.status !== "complete";
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
get blockedCount() {
|
|
375
|
-
return this.leafTickets.filter((t) => t.status !== "complete" && this.isBlocked(t)).length;
|
|
376
|
-
}
|
|
377
|
-
ticketByID(id) {
|
|
378
|
-
return this.ticketsByID.get(id);
|
|
379
|
-
}
|
|
380
|
-
issueByID(id) {
|
|
381
|
-
return this.issuesByID.get(id);
|
|
382
|
-
}
|
|
383
|
-
noteByID(id) {
|
|
384
|
-
return this.notesByID.get(id);
|
|
385
|
-
}
|
|
386
|
-
// --- Deletion Safety ---
|
|
387
|
-
/** IDs of tickets that list `ticketId` in their blockedBy. */
|
|
388
|
-
ticketsBlocking(ticketId) {
|
|
389
|
-
return (this.reverseBlocksMap.get(ticketId) ?? []).map((t) => t.id);
|
|
390
|
-
}
|
|
391
|
-
/** IDs of tickets that have `ticketId` as their parentTicket. */
|
|
392
|
-
childrenOf(ticketId) {
|
|
393
|
-
return (this.childrenByParent.get(ticketId) ?? []).map((t) => t.id);
|
|
394
|
-
}
|
|
395
|
-
/** IDs of issues that reference `ticketId` in relatedTickets. */
|
|
396
|
-
issuesReferencing(ticketId) {
|
|
397
|
-
return this.issues.filter((i) => i.relatedTickets.includes(ticketId)).map((i) => i.id);
|
|
398
|
-
}
|
|
399
|
-
// --- Private ---
|
|
400
|
-
/**
|
|
401
|
-
* Recursively collects all descendant leaf tickets of an umbrella.
|
|
402
|
-
* Uses a visited set to guard against cycles in malformed data.
|
|
403
|
-
*/
|
|
404
|
-
descendantLeaves(ticketId, visited) {
|
|
405
|
-
if (visited.has(ticketId)) return [];
|
|
406
|
-
visited.add(ticketId);
|
|
407
|
-
const directChildren = this.childrenByParent.get(ticketId) ?? [];
|
|
408
|
-
const leaves = [];
|
|
409
|
-
for (const child of directChildren) {
|
|
410
|
-
if (this.umbrellaIDs.has(child.id)) {
|
|
411
|
-
leaves.push(...this.descendantLeaves(child.id, visited));
|
|
412
|
-
} else {
|
|
413
|
-
leaves.push(child);
|
|
365
|
+
reverseBlocks(ticketId) {
|
|
366
|
+
return this.reverseBlocksMap.get(ticketId) ?? [];
|
|
414
367
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
368
|
+
/**
|
|
369
|
+
* A ticket is blocked if any blockedBy reference points to a non-complete ticket.
|
|
370
|
+
* Unknown blocker IDs treated as blocked (conservative — unknown dependency = assume not cleared).
|
|
371
|
+
*/
|
|
372
|
+
isBlocked(ticket) {
|
|
373
|
+
if (ticket.blockedBy.length === 0) return false;
|
|
374
|
+
return ticket.blockedBy.some((blockerID) => {
|
|
375
|
+
const blocker = this.ticketsByID.get(blockerID);
|
|
376
|
+
if (!blocker) return true;
|
|
377
|
+
return blocker.status !== "complete";
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
get blockedCount() {
|
|
381
|
+
return this.leafTickets.filter((t) => t.status !== "complete" && this.isBlocked(t)).length;
|
|
382
|
+
}
|
|
383
|
+
ticketByID(id) {
|
|
384
|
+
return this.ticketsByID.get(id);
|
|
385
|
+
}
|
|
386
|
+
issueByID(id) {
|
|
387
|
+
return this.issuesByID.get(id);
|
|
388
|
+
}
|
|
389
|
+
noteByID(id) {
|
|
390
|
+
return this.notesByID.get(id);
|
|
391
|
+
}
|
|
392
|
+
// --- Deletion Safety ---
|
|
393
|
+
/** IDs of tickets that list `ticketId` in their blockedBy. */
|
|
394
|
+
ticketsBlocking(ticketId) {
|
|
395
|
+
return (this.reverseBlocksMap.get(ticketId) ?? []).map((t) => t.id);
|
|
396
|
+
}
|
|
397
|
+
/** IDs of tickets that have `ticketId` as their parentTicket. */
|
|
398
|
+
childrenOf(ticketId) {
|
|
399
|
+
return (this.childrenByParent.get(ticketId) ?? []).map((t) => t.id);
|
|
400
|
+
}
|
|
401
|
+
/** IDs of issues that reference `ticketId` in relatedTickets. */
|
|
402
|
+
issuesReferencing(ticketId) {
|
|
403
|
+
return this.issues.filter((i) => i.relatedTickets.includes(ticketId)).map((i) => i.id);
|
|
404
|
+
}
|
|
405
|
+
// --- Private ---
|
|
406
|
+
/**
|
|
407
|
+
* Recursively collects all descendant leaf tickets of an umbrella.
|
|
408
|
+
* Uses a visited set to guard against cycles in malformed data.
|
|
409
|
+
*/
|
|
410
|
+
descendantLeaves(ticketId, visited) {
|
|
411
|
+
if (visited.has(ticketId)) return [];
|
|
412
|
+
visited.add(ticketId);
|
|
413
|
+
const directChildren = this.childrenByParent.get(ticketId) ?? [];
|
|
414
|
+
const leaves = [];
|
|
415
|
+
for (const child of directChildren) {
|
|
416
|
+
if (this.umbrellaIDs.has(child.id)) {
|
|
417
|
+
leaves.push(...this.descendantLeaves(child.id, visited));
|
|
418
|
+
} else {
|
|
419
|
+
leaves.push(child);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return leaves;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Shared aggregation logic for phase and umbrella status.
|
|
426
|
+
* - all complete → complete
|
|
427
|
+
* - any inprogress OR any complete (but not all) → inprogress
|
|
428
|
+
* - else → notstarted (nothing started)
|
|
429
|
+
*/
|
|
430
|
+
static aggregateStatus(tickets) {
|
|
431
|
+
if (tickets.length === 0) return "notstarted";
|
|
432
|
+
const allComplete = tickets.every((t) => t.status === "complete");
|
|
433
|
+
if (allComplete) return "complete";
|
|
434
|
+
const anyProgress = tickets.some((t) => t.status === "inprogress");
|
|
435
|
+
const anyComplete = tickets.some((t) => t.status === "complete");
|
|
436
|
+
if (anyProgress || anyComplete) return "inprogress";
|
|
437
|
+
return "notstarted";
|
|
438
|
+
}
|
|
439
|
+
};
|
|
432
440
|
}
|
|
433
|
-
};
|
|
441
|
+
});
|
|
434
442
|
|
|
435
443
|
// src/core/handover-parser.ts
|
|
436
444
|
import { readdir, readFile } from "fs/promises";
|
|
437
445
|
import { existsSync as existsSync2 } from "fs";
|
|
438
446
|
import { join as join2, relative, extname } from "path";
|
|
439
|
-
var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
|
|
440
|
-
var HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
|
|
441
447
|
async function listHandovers(handoversDir, root, warnings) {
|
|
442
448
|
if (!existsSync2(handoversDir)) return [];
|
|
443
449
|
let entries;
|
|
@@ -482,8 +488,32 @@ async function listHandovers(handoversDir, root, warnings) {
|
|
|
482
488
|
async function readHandover(handoversDir, filename) {
|
|
483
489
|
return readFile(join2(handoversDir, filename), "utf-8");
|
|
484
490
|
}
|
|
491
|
+
var HANDOVER_DATE_REGEX, HANDOVER_SEQ_REGEX;
|
|
492
|
+
var init_handover_parser = __esm({
|
|
493
|
+
"src/core/handover-parser.ts"() {
|
|
494
|
+
"use strict";
|
|
495
|
+
init_esm_shims();
|
|
496
|
+
HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
|
|
497
|
+
HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
485
500
|
|
|
486
501
|
// src/core/project-loader.ts
|
|
502
|
+
import {
|
|
503
|
+
readdir as readdir2,
|
|
504
|
+
readFile as readFile2,
|
|
505
|
+
writeFile,
|
|
506
|
+
rename,
|
|
507
|
+
unlink,
|
|
508
|
+
stat,
|
|
509
|
+
realpath,
|
|
510
|
+
lstat,
|
|
511
|
+
open,
|
|
512
|
+
mkdir
|
|
513
|
+
} from "fs/promises";
|
|
514
|
+
import { existsSync as existsSync3 } from "fs";
|
|
515
|
+
import { join as join3, resolve as resolve2, relative as relative2, extname as extname2, dirname as dirname2, basename } from "path";
|
|
516
|
+
import lockfile from "proper-lockfile";
|
|
487
517
|
async function loadProject(root, options) {
|
|
488
518
|
const absRoot = resolve2(root);
|
|
489
519
|
const wrapDir = join3(absRoot, ".story");
|
|
@@ -1025,78 +1055,21 @@ async function withLock(wrapDir, fn) {
|
|
|
1025
1055
|
}
|
|
1026
1056
|
}
|
|
1027
1057
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const y = d.getFullYear();
|
|
1042
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1043
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
1044
|
-
return `${y}-${m}-${day}`;
|
|
1045
|
-
}
|
|
1046
|
-
async function parseHandoverFilename(raw, handoversDir) {
|
|
1047
|
-
if (raw.includes("/") || raw.includes("\\") || raw.includes("..") || raw.includes("\0")) {
|
|
1048
|
-
throw new CliValidationError(
|
|
1049
|
-
"invalid_input",
|
|
1050
|
-
`Invalid handover filename "${raw}": contains path traversal characters`
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
if (extname3(raw) !== ".md") {
|
|
1054
|
-
throw new CliValidationError(
|
|
1055
|
-
"invalid_input",
|
|
1056
|
-
`Invalid handover filename "${raw}": must have .md extension`
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
const resolvedDir = resolve3(handoversDir);
|
|
1060
|
-
const resolvedCandidate = resolve3(handoversDir, raw);
|
|
1061
|
-
const rel = relative3(resolvedDir, resolvedCandidate);
|
|
1062
|
-
if (!rel || rel.startsWith("..") || resolve3(resolvedDir, rel) !== resolvedCandidate) {
|
|
1063
|
-
throw new CliValidationError(
|
|
1064
|
-
"invalid_input",
|
|
1065
|
-
`Invalid handover filename "${raw}": resolves outside handovers directory`
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
try {
|
|
1069
|
-
const stats = await lstat2(resolvedCandidate);
|
|
1070
|
-
if (stats.isSymbolicLink()) {
|
|
1071
|
-
throw new CliValidationError(
|
|
1072
|
-
"invalid_input",
|
|
1073
|
-
`Invalid handover filename "${raw}": symlinks not allowed`
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
} catch (err) {
|
|
1077
|
-
if (err instanceof CliValidationError) throw err;
|
|
1078
|
-
if (err.code !== "ENOENT") {
|
|
1079
|
-
throw new CliValidationError(
|
|
1080
|
-
"io_error",
|
|
1081
|
-
`Cannot check handover file "${raw}": ${err.message}`
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
return raw;
|
|
1086
|
-
}
|
|
1087
|
-
function normalizeTags(raw) {
|
|
1088
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1089
|
-
const result = [];
|
|
1090
|
-
for (const item of raw) {
|
|
1091
|
-
if (typeof item !== "string") continue;
|
|
1092
|
-
const normalized = item.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1093
|
-
if (normalized && !seen.has(normalized)) {
|
|
1094
|
-
seen.add(normalized);
|
|
1095
|
-
result.push(normalized);
|
|
1096
|
-
}
|
|
1058
|
+
var init_project_loader = __esm({
|
|
1059
|
+
"src/core/project-loader.ts"() {
|
|
1060
|
+
"use strict";
|
|
1061
|
+
init_esm_shims();
|
|
1062
|
+
init_ticket();
|
|
1063
|
+
init_issue();
|
|
1064
|
+
init_note();
|
|
1065
|
+
init_roadmap();
|
|
1066
|
+
init_config();
|
|
1067
|
+
init_types();
|
|
1068
|
+
init_project_state();
|
|
1069
|
+
init_errors();
|
|
1070
|
+
init_handover_parser();
|
|
1097
1071
|
}
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1072
|
+
});
|
|
1100
1073
|
|
|
1101
1074
|
// src/core/queries.ts
|
|
1102
1075
|
function nextTicket(state) {
|
|
@@ -1245,32 +1218,481 @@ function collectDescendantLeaves(ticketId, state, visited) {
|
|
|
1245
1218
|
}
|
|
1246
1219
|
return leaves;
|
|
1247
1220
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
USER_ERROR: 1,
|
|
1253
|
-
VALIDATION_ERROR: 2,
|
|
1254
|
-
PARTIAL: 3
|
|
1255
|
-
};
|
|
1256
|
-
function successEnvelope(data) {
|
|
1257
|
-
return { version: 1, data };
|
|
1258
|
-
}
|
|
1259
|
-
function errorEnvelope(code, message) {
|
|
1260
|
-
return { version: 1, error: { code, message } };
|
|
1261
|
-
}
|
|
1262
|
-
function escapeMarkdownInline(text) {
|
|
1263
|
-
return text.replace(/\\/g, "\\\\").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
1264
|
-
}
|
|
1265
|
-
function fencedBlock(content, lang) {
|
|
1266
|
-
let maxTicks = 2;
|
|
1267
|
-
const matches = content.match(/`+/g);
|
|
1268
|
-
if (matches) {
|
|
1269
|
-
for (const m of matches) {
|
|
1270
|
-
if (m.length > maxTicks) maxTicks = m.length;
|
|
1271
|
-
}
|
|
1221
|
+
var init_queries = __esm({
|
|
1222
|
+
"src/core/queries.ts"() {
|
|
1223
|
+
"use strict";
|
|
1224
|
+
init_esm_shims();
|
|
1272
1225
|
}
|
|
1273
|
-
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// src/core/snapshot.ts
|
|
1229
|
+
var snapshot_exports = {};
|
|
1230
|
+
__export(snapshot_exports, {
|
|
1231
|
+
SnapshotV1Schema: () => SnapshotV1Schema,
|
|
1232
|
+
buildRecap: () => buildRecap,
|
|
1233
|
+
diffStates: () => diffStates,
|
|
1234
|
+
loadLatestSnapshot: () => loadLatestSnapshot,
|
|
1235
|
+
saveSnapshot: () => saveSnapshot
|
|
1236
|
+
});
|
|
1237
|
+
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
|
|
1238
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1239
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
1240
|
+
import { z as z7 } from "zod";
|
|
1241
|
+
async function saveSnapshot(root, loadResult) {
|
|
1242
|
+
const absRoot = resolve5(root);
|
|
1243
|
+
const snapshotsDir = join5(absRoot, ".story", "snapshots");
|
|
1244
|
+
await mkdir3(snapshotsDir, { recursive: true });
|
|
1245
|
+
const { state, warnings } = loadResult;
|
|
1246
|
+
const now = /* @__PURE__ */ new Date();
|
|
1247
|
+
const filename = formatSnapshotFilename(now);
|
|
1248
|
+
const snapshot = {
|
|
1249
|
+
version: 1,
|
|
1250
|
+
createdAt: now.toISOString(),
|
|
1251
|
+
project: state.config.project,
|
|
1252
|
+
config: state.config,
|
|
1253
|
+
roadmap: state.roadmap,
|
|
1254
|
+
tickets: [...state.tickets],
|
|
1255
|
+
issues: [...state.issues],
|
|
1256
|
+
notes: [...state.notes],
|
|
1257
|
+
handoverFilenames: [...state.handoverFilenames],
|
|
1258
|
+
...warnings.length > 0 ? {
|
|
1259
|
+
warnings: warnings.map((w) => ({
|
|
1260
|
+
type: w.type,
|
|
1261
|
+
file: w.file,
|
|
1262
|
+
message: w.message
|
|
1263
|
+
}))
|
|
1264
|
+
} : {}
|
|
1265
|
+
};
|
|
1266
|
+
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
1267
|
+
const targetPath = join5(snapshotsDir, filename);
|
|
1268
|
+
const wrapDir = join5(absRoot, ".story");
|
|
1269
|
+
await guardPath(targetPath, wrapDir);
|
|
1270
|
+
await atomicWrite(targetPath, json);
|
|
1271
|
+
const pruned = await pruneSnapshots(snapshotsDir);
|
|
1272
|
+
const entries = await listSnapshotFiles(snapshotsDir);
|
|
1273
|
+
return { filename, retained: entries.length, pruned };
|
|
1274
|
+
}
|
|
1275
|
+
async function loadLatestSnapshot(root) {
|
|
1276
|
+
const snapshotsDir = join5(resolve5(root), ".story", "snapshots");
|
|
1277
|
+
if (!existsSync5(snapshotsDir)) return null;
|
|
1278
|
+
const files = await listSnapshotFiles(snapshotsDir);
|
|
1279
|
+
if (files.length === 0) return null;
|
|
1280
|
+
for (const filename of files) {
|
|
1281
|
+
try {
|
|
1282
|
+
const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
|
|
1283
|
+
const parsed = JSON.parse(content);
|
|
1284
|
+
const snapshot = SnapshotV1Schema.parse(parsed);
|
|
1285
|
+
return { snapshot, filename };
|
|
1286
|
+
} catch {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
function diffStates(snapshotState, currentState) {
|
|
1293
|
+
const snapTickets = new Map(snapshotState.tickets.map((t) => [t.id, t]));
|
|
1294
|
+
const curTickets = new Map(currentState.tickets.map((t) => [t.id, t]));
|
|
1295
|
+
const ticketsAdded = [];
|
|
1296
|
+
const ticketsRemoved = [];
|
|
1297
|
+
const ticketsStatusChanged = [];
|
|
1298
|
+
const ticketsDescriptionChanged = [];
|
|
1299
|
+
for (const [id, cur] of curTickets) {
|
|
1300
|
+
const snap = snapTickets.get(id);
|
|
1301
|
+
if (!snap) {
|
|
1302
|
+
ticketsAdded.push({ id, title: cur.title });
|
|
1303
|
+
} else {
|
|
1304
|
+
if (snap.status !== cur.status) {
|
|
1305
|
+
ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
1306
|
+
}
|
|
1307
|
+
if (snap.description !== cur.description) {
|
|
1308
|
+
ticketsDescriptionChanged.push({ id, title: cur.title });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (const [id, snap] of snapTickets) {
|
|
1313
|
+
if (!curTickets.has(id)) {
|
|
1314
|
+
ticketsRemoved.push({ id, title: snap.title });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const snapIssues = new Map(snapshotState.issues.map((i) => [i.id, i]));
|
|
1318
|
+
const curIssues = new Map(currentState.issues.map((i) => [i.id, i]));
|
|
1319
|
+
const issuesAdded = [];
|
|
1320
|
+
const issuesResolved = [];
|
|
1321
|
+
const issuesStatusChanged = [];
|
|
1322
|
+
const issuesImpactChanged = [];
|
|
1323
|
+
for (const [id, cur] of curIssues) {
|
|
1324
|
+
const snap = snapIssues.get(id);
|
|
1325
|
+
if (!snap) {
|
|
1326
|
+
issuesAdded.push({ id, title: cur.title });
|
|
1327
|
+
} else {
|
|
1328
|
+
if (snap.status !== cur.status) {
|
|
1329
|
+
if (cur.status === "resolved") {
|
|
1330
|
+
issuesResolved.push({ id, title: cur.title });
|
|
1331
|
+
} else {
|
|
1332
|
+
issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (snap.impact !== cur.impact) {
|
|
1336
|
+
issuesImpactChanged.push({ id, title: cur.title });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const snapBlockers = new Map(
|
|
1341
|
+
snapshotState.roadmap.blockers.map((b) => [b.name, b])
|
|
1342
|
+
);
|
|
1343
|
+
const curBlockers = new Map(
|
|
1344
|
+
currentState.roadmap.blockers.map((b) => [b.name, b])
|
|
1345
|
+
);
|
|
1346
|
+
const blockersAdded = [];
|
|
1347
|
+
const blockersCleared = [];
|
|
1348
|
+
for (const [name, cur] of curBlockers) {
|
|
1349
|
+
const snap = snapBlockers.get(name);
|
|
1350
|
+
if (!snap) {
|
|
1351
|
+
blockersAdded.push(name);
|
|
1352
|
+
} else if (!isBlockerCleared(snap) && isBlockerCleared(cur)) {
|
|
1353
|
+
blockersCleared.push(name);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const snapPhases = snapshotState.roadmap.phases;
|
|
1357
|
+
const curPhases = currentState.roadmap.phases;
|
|
1358
|
+
const snapPhaseMap = new Map(snapPhases.map((p) => [p.id, p]));
|
|
1359
|
+
const curPhaseMap = new Map(curPhases.map((p) => [p.id, p]));
|
|
1360
|
+
const phasesAdded = [];
|
|
1361
|
+
const phasesRemoved = [];
|
|
1362
|
+
const phasesStatusChanged = [];
|
|
1363
|
+
for (const [id, curPhase] of curPhaseMap) {
|
|
1364
|
+
const snapPhase = snapPhaseMap.get(id);
|
|
1365
|
+
if (!snapPhase) {
|
|
1366
|
+
phasesAdded.push({ id, name: curPhase.name });
|
|
1367
|
+
} else {
|
|
1368
|
+
const snapStatus = snapshotState.phaseStatus(id);
|
|
1369
|
+
const curStatus = currentState.phaseStatus(id);
|
|
1370
|
+
if (snapStatus !== curStatus) {
|
|
1371
|
+
phasesStatusChanged.push({
|
|
1372
|
+
id,
|
|
1373
|
+
name: curPhase.name,
|
|
1374
|
+
from: snapStatus,
|
|
1375
|
+
to: curStatus
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
for (const [id, snapPhase] of snapPhaseMap) {
|
|
1381
|
+
if (!curPhaseMap.has(id)) {
|
|
1382
|
+
phasesRemoved.push({ id, name: snapPhase.name });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const snapNotes = new Map(snapshotState.notes.map((n) => [n.id, n]));
|
|
1386
|
+
const curNotes = new Map(currentState.notes.map((n) => [n.id, n]));
|
|
1387
|
+
const notesAdded = [];
|
|
1388
|
+
const notesRemoved = [];
|
|
1389
|
+
const notesUpdated = [];
|
|
1390
|
+
for (const [id, cur] of curNotes) {
|
|
1391
|
+
const snap = snapNotes.get(id);
|
|
1392
|
+
if (!snap) {
|
|
1393
|
+
notesAdded.push({ id, title: cur.title });
|
|
1394
|
+
} else {
|
|
1395
|
+
const changedFields = [];
|
|
1396
|
+
if (snap.title !== cur.title) changedFields.push("title");
|
|
1397
|
+
if (snap.content !== cur.content) changedFields.push("content");
|
|
1398
|
+
if (JSON.stringify([...snap.tags].sort()) !== JSON.stringify([...cur.tags].sort())) changedFields.push("tags");
|
|
1399
|
+
if (snap.status !== cur.status) changedFields.push("status");
|
|
1400
|
+
if (changedFields.length > 0) {
|
|
1401
|
+
notesUpdated.push({ id, title: cur.title, changedFields });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const [id, snap] of snapNotes) {
|
|
1406
|
+
if (!curNotes.has(id)) {
|
|
1407
|
+
notesRemoved.push({ id, title: snap.title });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const snapHandovers = new Set(snapshotState.handoverFilenames);
|
|
1411
|
+
const curHandovers = new Set(currentState.handoverFilenames);
|
|
1412
|
+
const handoversAdded = [];
|
|
1413
|
+
const handoversRemoved = [];
|
|
1414
|
+
for (const h of curHandovers) {
|
|
1415
|
+
if (!snapHandovers.has(h)) handoversAdded.push(h);
|
|
1416
|
+
}
|
|
1417
|
+
for (const h of snapHandovers) {
|
|
1418
|
+
if (!curHandovers.has(h)) handoversRemoved.push(h);
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
|
|
1422
|
+
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
|
|
1423
|
+
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
1424
|
+
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
|
|
1425
|
+
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated },
|
|
1426
|
+
handovers: { added: handoversAdded, removed: handoversRemoved }
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function buildRecap(currentState, snapshotInfo) {
|
|
1430
|
+
const next = nextTicket(currentState);
|
|
1431
|
+
const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
|
|
1432
|
+
const highSeverityIssues = currentState.issues.filter(
|
|
1433
|
+
(i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
|
|
1434
|
+
).map((i) => ({ id: i.id, title: i.title, severity: i.severity }));
|
|
1435
|
+
if (!snapshotInfo) {
|
|
1436
|
+
return {
|
|
1437
|
+
snapshot: null,
|
|
1438
|
+
changes: null,
|
|
1439
|
+
suggestedActions: {
|
|
1440
|
+
nextTicket: nextTicketAction,
|
|
1441
|
+
highSeverityIssues,
|
|
1442
|
+
recentlyClearedBlockers: []
|
|
1443
|
+
},
|
|
1444
|
+
partial: false
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
const { snapshot, filename } = snapshotInfo;
|
|
1448
|
+
const snapshotState = new ProjectState({
|
|
1449
|
+
tickets: snapshot.tickets,
|
|
1450
|
+
issues: snapshot.issues,
|
|
1451
|
+
notes: snapshot.notes ?? [],
|
|
1452
|
+
roadmap: snapshot.roadmap,
|
|
1453
|
+
config: snapshot.config,
|
|
1454
|
+
handoverFilenames: snapshot.handoverFilenames ?? []
|
|
1455
|
+
});
|
|
1456
|
+
const changes = diffStates(snapshotState, currentState);
|
|
1457
|
+
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
1458
|
+
return {
|
|
1459
|
+
snapshot: { filename, createdAt: snapshot.createdAt },
|
|
1460
|
+
changes,
|
|
1461
|
+
suggestedActions: {
|
|
1462
|
+
nextTicket: nextTicketAction,
|
|
1463
|
+
highSeverityIssues,
|
|
1464
|
+
recentlyClearedBlockers
|
|
1465
|
+
},
|
|
1466
|
+
partial: (snapshot.warnings ?? []).length > 0
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function formatSnapshotFilename(date) {
|
|
1470
|
+
const y = date.getUTCFullYear();
|
|
1471
|
+
const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1472
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
1473
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
1474
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
1475
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
1476
|
+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
1477
|
+
return `${y}-${mo}-${d}T${h}-${mi}-${s}-${ms}.json`;
|
|
1478
|
+
}
|
|
1479
|
+
async function listSnapshotFiles(dir) {
|
|
1480
|
+
try {
|
|
1481
|
+
const entries = await readdir3(dir);
|
|
1482
|
+
return entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().reverse();
|
|
1483
|
+
} catch {
|
|
1484
|
+
return [];
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function pruneSnapshots(dir) {
|
|
1488
|
+
const files = await listSnapshotFiles(dir);
|
|
1489
|
+
if (files.length <= MAX_SNAPSHOTS) return 0;
|
|
1490
|
+
const toRemove = files.slice(MAX_SNAPSHOTS);
|
|
1491
|
+
for (const f of toRemove) {
|
|
1492
|
+
try {
|
|
1493
|
+
await unlink2(join5(dir, f));
|
|
1494
|
+
} catch {
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return toRemove.length;
|
|
1498
|
+
}
|
|
1499
|
+
var LoadWarningSchema, SnapshotV1Schema, MAX_SNAPSHOTS;
|
|
1500
|
+
var init_snapshot = __esm({
|
|
1501
|
+
"src/core/snapshot.ts"() {
|
|
1502
|
+
"use strict";
|
|
1503
|
+
init_esm_shims();
|
|
1504
|
+
init_ticket();
|
|
1505
|
+
init_issue();
|
|
1506
|
+
init_note();
|
|
1507
|
+
init_roadmap();
|
|
1508
|
+
init_config();
|
|
1509
|
+
init_project_state();
|
|
1510
|
+
init_queries();
|
|
1511
|
+
init_project_loader();
|
|
1512
|
+
LoadWarningSchema = z7.object({
|
|
1513
|
+
type: z7.string(),
|
|
1514
|
+
file: z7.string(),
|
|
1515
|
+
message: z7.string()
|
|
1516
|
+
});
|
|
1517
|
+
SnapshotV1Schema = z7.object({
|
|
1518
|
+
version: z7.literal(1),
|
|
1519
|
+
createdAt: z7.string().datetime({ offset: true }),
|
|
1520
|
+
project: z7.string(),
|
|
1521
|
+
config: ConfigSchema,
|
|
1522
|
+
roadmap: RoadmapSchema,
|
|
1523
|
+
tickets: z7.array(TicketSchema),
|
|
1524
|
+
issues: z7.array(IssueSchema),
|
|
1525
|
+
notes: z7.array(NoteSchema).optional().default([]),
|
|
1526
|
+
handoverFilenames: z7.array(z7.string()).optional().default([]),
|
|
1527
|
+
warnings: z7.array(LoadWarningSchema).optional()
|
|
1528
|
+
});
|
|
1529
|
+
MAX_SNAPSHOTS = 20;
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
// src/mcp/index.ts
|
|
1534
|
+
init_esm_shims();
|
|
1535
|
+
import { realpathSync as realpathSync2, existsSync as existsSync8 } from "fs";
|
|
1536
|
+
import { resolve as resolve8, join as join11, isAbsolute } from "path";
|
|
1537
|
+
import { z as z10 } from "zod";
|
|
1538
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1539
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1540
|
+
|
|
1541
|
+
// src/core/project-root-discovery.ts
|
|
1542
|
+
init_esm_shims();
|
|
1543
|
+
init_errors();
|
|
1544
|
+
import { existsSync, accessSync, constants } from "fs";
|
|
1545
|
+
import { resolve, dirname, join } from "path";
|
|
1546
|
+
var ENV_VAR = "CLAUDESTORY_PROJECT_ROOT";
|
|
1547
|
+
var STORY_DIR = ".story";
|
|
1548
|
+
var CONFIG_PATH = ".story/config.json";
|
|
1549
|
+
function discoverProjectRoot(startDir) {
|
|
1550
|
+
const envRoot = process.env[ENV_VAR];
|
|
1551
|
+
if (envRoot) {
|
|
1552
|
+
const resolved = resolve(envRoot);
|
|
1553
|
+
return checkRoot(resolved);
|
|
1554
|
+
}
|
|
1555
|
+
let current = resolve(startDir ?? process.cwd());
|
|
1556
|
+
for (; ; ) {
|
|
1557
|
+
const result = checkRoot(current);
|
|
1558
|
+
if (result) return result;
|
|
1559
|
+
const parent = dirname(current);
|
|
1560
|
+
if (parent === current) break;
|
|
1561
|
+
current = parent;
|
|
1562
|
+
}
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
function checkRoot(candidate) {
|
|
1566
|
+
if (existsSync(join(candidate, CONFIG_PATH))) {
|
|
1567
|
+
return candidate;
|
|
1568
|
+
}
|
|
1569
|
+
if (existsSync(join(candidate, STORY_DIR))) {
|
|
1570
|
+
try {
|
|
1571
|
+
accessSync(join(candidate, STORY_DIR), constants.R_OK);
|
|
1572
|
+
} catch {
|
|
1573
|
+
throw new ProjectLoaderError(
|
|
1574
|
+
"io_error",
|
|
1575
|
+
`Permission denied: cannot read .story/ in ${candidate}`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/mcp/tools.ts
|
|
1583
|
+
init_esm_shims();
|
|
1584
|
+
init_project_loader();
|
|
1585
|
+
init_errors();
|
|
1586
|
+
import { z as z9 } from "zod";
|
|
1587
|
+
import { join as join9 } from "path";
|
|
1588
|
+
|
|
1589
|
+
// src/cli/helpers.ts
|
|
1590
|
+
init_esm_shims();
|
|
1591
|
+
init_types();
|
|
1592
|
+
import { resolve as resolve3, relative as relative3, extname as extname3 } from "path";
|
|
1593
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
1594
|
+
var CliValidationError = class extends Error {
|
|
1595
|
+
constructor(code, message) {
|
|
1596
|
+
super(message);
|
|
1597
|
+
this.code = code;
|
|
1598
|
+
this.name = "CliValidationError";
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
function todayISO() {
|
|
1602
|
+
const d = /* @__PURE__ */ new Date();
|
|
1603
|
+
const y = d.getFullYear();
|
|
1604
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1605
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1606
|
+
return `${y}-${m}-${day}`;
|
|
1607
|
+
}
|
|
1608
|
+
async function parseHandoverFilename(raw, handoversDir) {
|
|
1609
|
+
if (raw.includes("/") || raw.includes("\\") || raw.includes("..") || raw.includes("\0")) {
|
|
1610
|
+
throw new CliValidationError(
|
|
1611
|
+
"invalid_input",
|
|
1612
|
+
`Invalid handover filename "${raw}": contains path traversal characters`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
if (extname3(raw) !== ".md") {
|
|
1616
|
+
throw new CliValidationError(
|
|
1617
|
+
"invalid_input",
|
|
1618
|
+
`Invalid handover filename "${raw}": must have .md extension`
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
const resolvedDir = resolve3(handoversDir);
|
|
1622
|
+
const resolvedCandidate = resolve3(handoversDir, raw);
|
|
1623
|
+
const rel = relative3(resolvedDir, resolvedCandidate);
|
|
1624
|
+
if (!rel || rel.startsWith("..") || resolve3(resolvedDir, rel) !== resolvedCandidate) {
|
|
1625
|
+
throw new CliValidationError(
|
|
1626
|
+
"invalid_input",
|
|
1627
|
+
`Invalid handover filename "${raw}": resolves outside handovers directory`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
const stats = await lstat2(resolvedCandidate);
|
|
1632
|
+
if (stats.isSymbolicLink()) {
|
|
1633
|
+
throw new CliValidationError(
|
|
1634
|
+
"invalid_input",
|
|
1635
|
+
`Invalid handover filename "${raw}": symlinks not allowed`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
if (err instanceof CliValidationError) throw err;
|
|
1640
|
+
if (err.code !== "ENOENT") {
|
|
1641
|
+
throw new CliValidationError(
|
|
1642
|
+
"io_error",
|
|
1643
|
+
`Cannot check handover file "${raw}": ${err.message}`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return raw;
|
|
1648
|
+
}
|
|
1649
|
+
function normalizeTags(raw) {
|
|
1650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1651
|
+
const result = [];
|
|
1652
|
+
for (const item of raw) {
|
|
1653
|
+
if (typeof item !== "string") continue;
|
|
1654
|
+
const normalized = item.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1655
|
+
if (normalized && !seen.has(normalized)) {
|
|
1656
|
+
seen.add(normalized);
|
|
1657
|
+
result.push(normalized);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return result;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/mcp/tools.ts
|
|
1664
|
+
init_types();
|
|
1665
|
+
|
|
1666
|
+
// src/cli/commands/status.ts
|
|
1667
|
+
init_esm_shims();
|
|
1668
|
+
|
|
1669
|
+
// src/core/output-formatter.ts
|
|
1670
|
+
init_esm_shims();
|
|
1671
|
+
init_queries();
|
|
1672
|
+
var ExitCode = {
|
|
1673
|
+
OK: 0,
|
|
1674
|
+
USER_ERROR: 1,
|
|
1675
|
+
VALIDATION_ERROR: 2,
|
|
1676
|
+
PARTIAL: 3
|
|
1677
|
+
};
|
|
1678
|
+
function successEnvelope(data) {
|
|
1679
|
+
return { version: 1, data };
|
|
1680
|
+
}
|
|
1681
|
+
function errorEnvelope(code, message) {
|
|
1682
|
+
return { version: 1, error: { code, message } };
|
|
1683
|
+
}
|
|
1684
|
+
function escapeMarkdownInline(text) {
|
|
1685
|
+
return text.replace(/\\/g, "\\\\").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
1686
|
+
}
|
|
1687
|
+
function fencedBlock(content, lang) {
|
|
1688
|
+
let maxTicks = 2;
|
|
1689
|
+
const matches = content.match(/`+/g);
|
|
1690
|
+
if (matches) {
|
|
1691
|
+
for (const m of matches) {
|
|
1692
|
+
if (m.length > maxTicks) maxTicks = m.length;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const fence = "`".repeat(maxTicks + 1);
|
|
1274
1696
|
return `${fence}${lang ?? ""}
|
|
1275
1697
|
${content}
|
|
1276
1698
|
${fence}`;
|
|
@@ -2012,7 +2434,11 @@ function handleStatus(ctx) {
|
|
|
2012
2434
|
return { output: formatStatus(ctx.state, ctx.format) };
|
|
2013
2435
|
}
|
|
2014
2436
|
|
|
2437
|
+
// src/cli/commands/validate.ts
|
|
2438
|
+
init_esm_shims();
|
|
2439
|
+
|
|
2015
2440
|
// src/core/validation.ts
|
|
2441
|
+
init_esm_shims();
|
|
2016
2442
|
function validateProject(state) {
|
|
2017
2443
|
const findings = [];
|
|
2018
2444
|
const phaseIDs = new Set(state.roadmap.phases.map((p) => p.id));
|
|
@@ -2278,9 +2704,12 @@ function handleValidate(ctx) {
|
|
|
2278
2704
|
}
|
|
2279
2705
|
|
|
2280
2706
|
// src/cli/commands/handover.ts
|
|
2707
|
+
init_esm_shims();
|
|
2708
|
+
init_handover_parser();
|
|
2281
2709
|
import { existsSync as existsSync4 } from "fs";
|
|
2282
2710
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2283
2711
|
import { join as join4, resolve as resolve4 } from "path";
|
|
2712
|
+
init_project_loader();
|
|
2284
2713
|
function handleHandoverList(ctx) {
|
|
2285
2714
|
return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
|
|
2286
2715
|
}
|
|
@@ -2371,9 +2800,9 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
2371
2800
|
const datePrefix = `${date}-`;
|
|
2372
2801
|
const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
|
|
2373
2802
|
let maxSeq = 0;
|
|
2374
|
-
const { readdirSync } = await import("fs");
|
|
2803
|
+
const { readdirSync: readdirSync2 } = await import("fs");
|
|
2375
2804
|
try {
|
|
2376
|
-
for (const f of
|
|
2805
|
+
for (const f of readdirSync2(handoversDir)) {
|
|
2377
2806
|
const m = f.match(seqRegex);
|
|
2378
2807
|
if (m) {
|
|
2379
2808
|
const n = parseInt(m[1], 10);
|
|
@@ -2411,11 +2840,20 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
|
|
|
2411
2840
|
}
|
|
2412
2841
|
|
|
2413
2842
|
// src/cli/commands/blocker.ts
|
|
2843
|
+
init_esm_shims();
|
|
2844
|
+
init_queries();
|
|
2845
|
+
init_project_loader();
|
|
2414
2846
|
function handleBlockerList(ctx) {
|
|
2415
2847
|
return { output: formatBlockerList(ctx.state.roadmap, ctx.format) };
|
|
2416
2848
|
}
|
|
2417
2849
|
|
|
2850
|
+
// src/cli/commands/ticket.ts
|
|
2851
|
+
init_esm_shims();
|
|
2852
|
+
init_queries();
|
|
2853
|
+
|
|
2418
2854
|
// src/core/id-allocation.ts
|
|
2855
|
+
init_esm_shims();
|
|
2856
|
+
init_types();
|
|
2419
2857
|
var TICKET_NUMERIC_REGEX = /^T-(\d+)[a-z]?$/;
|
|
2420
2858
|
var ISSUE_NUMERIC_REGEX = /^ISS-(\d+)$/;
|
|
2421
2859
|
var NOTE_NUMERIC_REGEX = /^N-(\d+)$/;
|
|
@@ -2462,6 +2900,9 @@ function nextOrder(phaseId, state) {
|
|
|
2462
2900
|
}
|
|
2463
2901
|
|
|
2464
2902
|
// src/cli/commands/ticket.ts
|
|
2903
|
+
init_project_state();
|
|
2904
|
+
init_project_loader();
|
|
2905
|
+
init_types();
|
|
2465
2906
|
function handleTicketList(filters, ctx) {
|
|
2466
2907
|
let tickets = [...ctx.state.leafTickets];
|
|
2467
2908
|
if (filters.status) {
|
|
@@ -2485,240 +2926,72 @@ function handleTicketList(filters, ctx) {
|
|
|
2485
2926
|
}
|
|
2486
2927
|
tickets = tickets.filter((t) => t.type === filters.type);
|
|
2487
2928
|
}
|
|
2488
|
-
return { output: formatTicketList(tickets, ctx.format) };
|
|
2489
|
-
}
|
|
2490
|
-
function handleTicketGet(id, ctx) {
|
|
2491
|
-
const ticket = ctx.state.ticketByID(id);
|
|
2492
|
-
if (!ticket) {
|
|
2493
|
-
return {
|
|
2494
|
-
output: formatError("not_found", `Ticket ${id} not found`, ctx.format),
|
|
2495
|
-
exitCode: ExitCode.USER_ERROR,
|
|
2496
|
-
errorCode: "not_found"
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
return { output: formatTicket(ticket, ctx.state, ctx.format) };
|
|
2500
|
-
}
|
|
2501
|
-
function handleTicketNext(ctx, count = 1) {
|
|
2502
|
-
if (count <= 1) {
|
|
2503
|
-
const outcome2 = nextTicket(ctx.state);
|
|
2504
|
-
const exitCode2 = outcome2.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
|
|
2505
|
-
return { output: formatNextTicketOutcome(outcome2, ctx.state, ctx.format), exitCode: exitCode2 };
|
|
2506
|
-
}
|
|
2507
|
-
const outcome = nextTickets(ctx.state, count);
|
|
2508
|
-
const exitCode = outcome.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
|
|
2509
|
-
return { output: formatNextTicketsOutcome(outcome, ctx.state, ctx.format), exitCode };
|
|
2510
|
-
}
|
|
2511
|
-
function handleTicketBlocked(ctx) {
|
|
2512
|
-
const blocked = blockedTickets(ctx.state);
|
|
2513
|
-
return { output: formatBlockedTickets(blocked, ctx.state, ctx.format) };
|
|
2514
|
-
}
|
|
2515
|
-
function validatePhase(phase, ctx) {
|
|
2516
|
-
if (phase !== null && !ctx.state.roadmap.phases.some((p) => p.id === phase)) {
|
|
2517
|
-
throw new CliValidationError("invalid_input", `Phase "${phase}" not found in roadmap`);
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
function validateBlockedBy(ids, ticketId, state) {
|
|
2521
|
-
for (const bid of ids) {
|
|
2522
|
-
if (bid === ticketId) {
|
|
2523
|
-
throw new CliValidationError("invalid_input", `Ticket cannot block itself: ${bid}`);
|
|
2524
|
-
}
|
|
2525
|
-
const blocker = state.ticketByID(bid);
|
|
2526
|
-
if (!blocker) {
|
|
2527
|
-
throw new CliValidationError("invalid_input", `Blocked-by ticket ${bid} not found`);
|
|
2528
|
-
}
|
|
2529
|
-
if (state.umbrellaIDs.has(bid)) {
|
|
2530
|
-
throw new CliValidationError("invalid_input", `Cannot block on umbrella ticket ${bid}. Use leaf tickets instead.`);
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
function validateParentTicket(parentId, ticketId, state) {
|
|
2535
|
-
if (parentId === ticketId) {
|
|
2536
|
-
throw new CliValidationError("invalid_input", `Ticket cannot be its own parent`);
|
|
2537
|
-
}
|
|
2538
|
-
if (!state.ticketByID(parentId)) {
|
|
2539
|
-
throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
function validatePostWriteState(candidate, state, isCreate) {
|
|
2543
|
-
const existingTickets = [...state.tickets];
|
|
2544
|
-
if (isCreate) {
|
|
2545
|
-
existingTickets.push(candidate);
|
|
2546
|
-
} else {
|
|
2547
|
-
const idx = existingTickets.findIndex((t) => t.id === candidate.id);
|
|
2548
|
-
if (idx >= 0) existingTickets[idx] = candidate;
|
|
2549
|
-
else existingTickets.push(candidate);
|
|
2550
|
-
}
|
|
2551
|
-
const postState = new ProjectState({
|
|
2552
|
-
tickets: existingTickets,
|
|
2553
|
-
issues: [...state.issues],
|
|
2554
|
-
notes: [...state.notes],
|
|
2555
|
-
roadmap: state.roadmap,
|
|
2556
|
-
config: state.config,
|
|
2557
|
-
handoverFilenames: [...state.handoverFilenames]
|
|
2558
|
-
});
|
|
2559
|
-
const result = validateProject(postState);
|
|
2560
|
-
if (!result.valid) {
|
|
2561
|
-
const errors = result.findings.filter((f) => f.level === "error");
|
|
2562
|
-
const msg = errors.map((f) => f.message).join("; ");
|
|
2563
|
-
throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
async function handleTicketCreate(args, format, root) {
|
|
2567
|
-
if (!TICKET_TYPES.includes(args.type)) {
|
|
2568
|
-
throw new CliValidationError(
|
|
2569
|
-
"invalid_input",
|
|
2570
|
-
`Unknown ticket type "${args.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
2571
|
-
);
|
|
2572
|
-
}
|
|
2573
|
-
let createdTicket;
|
|
2574
|
-
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2575
|
-
validatePhase(args.phase, { state });
|
|
2576
|
-
if (args.blockedBy.length > 0) {
|
|
2577
|
-
validateBlockedBy(args.blockedBy, "", state);
|
|
2578
|
-
}
|
|
2579
|
-
if (args.parentTicket) {
|
|
2580
|
-
validateParentTicket(args.parentTicket, "", state);
|
|
2581
|
-
}
|
|
2582
|
-
const id = nextTicketID(state.tickets);
|
|
2583
|
-
const order = nextOrder(args.phase, state);
|
|
2584
|
-
const ticket = {
|
|
2585
|
-
id,
|
|
2586
|
-
title: args.title,
|
|
2587
|
-
description: args.description,
|
|
2588
|
-
type: args.type,
|
|
2589
|
-
status: "open",
|
|
2590
|
-
phase: args.phase,
|
|
2591
|
-
order,
|
|
2592
|
-
createdDate: todayISO(),
|
|
2593
|
-
completedDate: null,
|
|
2594
|
-
blockedBy: args.blockedBy,
|
|
2595
|
-
parentTicket: args.parentTicket ?? void 0
|
|
2596
|
-
};
|
|
2597
|
-
validatePostWriteState(ticket, state, true);
|
|
2598
|
-
await writeTicketUnlocked(ticket, root);
|
|
2599
|
-
createdTicket = ticket;
|
|
2600
|
-
});
|
|
2601
|
-
if (!createdTicket) throw new Error("Ticket not created");
|
|
2602
|
-
if (format === "json") {
|
|
2603
|
-
return { output: JSON.stringify(successEnvelope(createdTicket), null, 2) };
|
|
2604
|
-
}
|
|
2605
|
-
return { output: `Created ticket ${createdTicket.id}: ${createdTicket.title}` };
|
|
2606
|
-
}
|
|
2607
|
-
async function handleTicketUpdate(id, updates, format, root) {
|
|
2608
|
-
if (updates.status && !TICKET_STATUSES.includes(updates.status)) {
|
|
2609
|
-
throw new CliValidationError(
|
|
2610
|
-
"invalid_input",
|
|
2611
|
-
`Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
|
|
2612
|
-
);
|
|
2613
|
-
}
|
|
2614
|
-
if (updates.type !== void 0 && !TICKET_TYPES.includes(updates.type)) {
|
|
2615
|
-
throw new CliValidationError(
|
|
2616
|
-
"invalid_input",
|
|
2617
|
-
`Unknown ticket type "${updates.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
2618
|
-
);
|
|
2619
|
-
}
|
|
2620
|
-
let updatedTicket;
|
|
2621
|
-
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2622
|
-
const existing = state.ticketByID(id);
|
|
2623
|
-
if (!existing) {
|
|
2624
|
-
throw new CliValidationError("not_found", `Ticket ${id} not found`);
|
|
2625
|
-
}
|
|
2626
|
-
if (updates.phase !== void 0) {
|
|
2627
|
-
validatePhase(updates.phase, { state });
|
|
2628
|
-
}
|
|
2629
|
-
if (updates.blockedBy) {
|
|
2630
|
-
validateBlockedBy(updates.blockedBy, id, state);
|
|
2631
|
-
}
|
|
2632
|
-
if (updates.parentTicket) {
|
|
2633
|
-
validateParentTicket(updates.parentTicket, id, state);
|
|
2634
|
-
}
|
|
2635
|
-
const statusChanges = {};
|
|
2636
|
-
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
2637
|
-
statusChanges.status = updates.status;
|
|
2638
|
-
if (updates.status === "complete" && existing.status !== "complete") {
|
|
2639
|
-
statusChanges.completedDate = todayISO();
|
|
2640
|
-
} else if (updates.status !== "complete" && existing.status === "complete") {
|
|
2641
|
-
statusChanges.completedDate = null;
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
const ticket = {
|
|
2645
|
-
...existing,
|
|
2646
|
-
...updates.title !== void 0 && { title: updates.title },
|
|
2647
|
-
...updates.type !== void 0 && { type: updates.type },
|
|
2648
|
-
...updates.description !== void 0 && { description: updates.description },
|
|
2649
|
-
...updates.phase !== void 0 && { phase: updates.phase },
|
|
2650
|
-
...updates.order !== void 0 && { order: updates.order },
|
|
2651
|
-
...updates.blockedBy !== void 0 && { blockedBy: updates.blockedBy },
|
|
2652
|
-
...updates.parentTicket !== void 0 && { parentTicket: updates.parentTicket },
|
|
2653
|
-
...statusChanges
|
|
2654
|
-
};
|
|
2655
|
-
validatePostWriteState(ticket, state, false);
|
|
2656
|
-
await writeTicketUnlocked(ticket, root);
|
|
2657
|
-
updatedTicket = ticket;
|
|
2658
|
-
});
|
|
2659
|
-
if (!updatedTicket) throw new Error("Ticket not updated");
|
|
2660
|
-
if (format === "json") {
|
|
2661
|
-
return { output: JSON.stringify(successEnvelope(updatedTicket), null, 2) };
|
|
2662
|
-
}
|
|
2663
|
-
return { output: `Updated ticket ${updatedTicket.id}: ${updatedTicket.title}` };
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
// src/cli/commands/issue.ts
|
|
2667
|
-
function handleIssueList(filters, ctx) {
|
|
2668
|
-
let issues = [...ctx.state.issues];
|
|
2669
|
-
if (filters.status) {
|
|
2670
|
-
if (!ISSUE_STATUSES.includes(filters.status)) {
|
|
2671
|
-
throw new CliValidationError(
|
|
2672
|
-
"invalid_input",
|
|
2673
|
-
`Unknown issue status "${filters.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
|
|
2674
|
-
);
|
|
2675
|
-
}
|
|
2676
|
-
issues = issues.filter((i) => i.status === filters.status);
|
|
2677
|
-
}
|
|
2678
|
-
if (filters.severity) {
|
|
2679
|
-
if (!ISSUE_SEVERITIES.includes(filters.severity)) {
|
|
2680
|
-
throw new CliValidationError(
|
|
2681
|
-
"invalid_input",
|
|
2682
|
-
`Unknown issue severity "${filters.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
|
|
2683
|
-
);
|
|
2684
|
-
}
|
|
2685
|
-
issues = issues.filter((i) => i.severity === filters.severity);
|
|
2686
|
-
}
|
|
2687
|
-
if (filters.component) {
|
|
2688
|
-
issues = issues.filter((i) => i.components.includes(filters.component));
|
|
2689
|
-
}
|
|
2690
|
-
return { output: formatIssueList(issues, ctx.format) };
|
|
2691
|
-
}
|
|
2692
|
-
function handleIssueGet(id, ctx) {
|
|
2693
|
-
const issue = ctx.state.issueByID(id);
|
|
2694
|
-
if (!issue) {
|
|
2929
|
+
return { output: formatTicketList(tickets, ctx.format) };
|
|
2930
|
+
}
|
|
2931
|
+
function handleTicketGet(id, ctx) {
|
|
2932
|
+
const ticket = ctx.state.ticketByID(id);
|
|
2933
|
+
if (!ticket) {
|
|
2695
2934
|
return {
|
|
2696
|
-
output: formatError("not_found", `
|
|
2935
|
+
output: formatError("not_found", `Ticket ${id} not found`, ctx.format),
|
|
2697
2936
|
exitCode: ExitCode.USER_ERROR,
|
|
2698
2937
|
errorCode: "not_found"
|
|
2699
2938
|
};
|
|
2700
2939
|
}
|
|
2701
|
-
return { output:
|
|
2940
|
+
return { output: formatTicket(ticket, ctx.state, ctx.format) };
|
|
2702
2941
|
}
|
|
2703
|
-
function
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2942
|
+
function handleTicketNext(ctx, count = 1) {
|
|
2943
|
+
if (count <= 1) {
|
|
2944
|
+
const outcome2 = nextTicket(ctx.state);
|
|
2945
|
+
const exitCode2 = outcome2.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
|
|
2946
|
+
return { output: formatNextTicketOutcome(outcome2, ctx.state, ctx.format), exitCode: exitCode2 };
|
|
2947
|
+
}
|
|
2948
|
+
const outcome = nextTickets(ctx.state, count);
|
|
2949
|
+
const exitCode = outcome.kind === "found" ? ExitCode.OK : ExitCode.USER_ERROR;
|
|
2950
|
+
return { output: formatNextTicketsOutcome(outcome, ctx.state, ctx.format), exitCode };
|
|
2951
|
+
}
|
|
2952
|
+
function handleTicketBlocked(ctx) {
|
|
2953
|
+
const blocked = blockedTickets(ctx.state);
|
|
2954
|
+
return { output: formatBlockedTickets(blocked, ctx.state, ctx.format) };
|
|
2955
|
+
}
|
|
2956
|
+
function validatePhase(phase, ctx) {
|
|
2957
|
+
if (phase !== null && !ctx.state.roadmap.phases.some((p) => p.id === phase)) {
|
|
2958
|
+
throw new CliValidationError("invalid_input", `Phase "${phase}" not found in roadmap`);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
function validateBlockedBy(ids, ticketId, state) {
|
|
2962
|
+
for (const bid of ids) {
|
|
2963
|
+
if (bid === ticketId) {
|
|
2964
|
+
throw new CliValidationError("invalid_input", `Ticket cannot block itself: ${bid}`);
|
|
2965
|
+
}
|
|
2966
|
+
const blocker = state.ticketByID(bid);
|
|
2967
|
+
if (!blocker) {
|
|
2968
|
+
throw new CliValidationError("invalid_input", `Blocked-by ticket ${bid} not found`);
|
|
2969
|
+
}
|
|
2970
|
+
if (state.umbrellaIDs.has(bid)) {
|
|
2971
|
+
throw new CliValidationError("invalid_input", `Cannot block on umbrella ticket ${bid}. Use leaf tickets instead.`);
|
|
2707
2972
|
}
|
|
2708
2973
|
}
|
|
2709
2974
|
}
|
|
2710
|
-
function
|
|
2711
|
-
|
|
2975
|
+
function validateParentTicket(parentId, ticketId, state) {
|
|
2976
|
+
if (parentId === ticketId) {
|
|
2977
|
+
throw new CliValidationError("invalid_input", `Ticket cannot be its own parent`);
|
|
2978
|
+
}
|
|
2979
|
+
if (!state.ticketByID(parentId)) {
|
|
2980
|
+
throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function validatePostWriteState(candidate, state, isCreate) {
|
|
2984
|
+
const existingTickets = [...state.tickets];
|
|
2712
2985
|
if (isCreate) {
|
|
2713
|
-
|
|
2986
|
+
existingTickets.push(candidate);
|
|
2714
2987
|
} else {
|
|
2715
|
-
const idx =
|
|
2716
|
-
if (idx >= 0)
|
|
2717
|
-
else
|
|
2988
|
+
const idx = existingTickets.findIndex((t) => t.id === candidate.id);
|
|
2989
|
+
if (idx >= 0) existingTickets[idx] = candidate;
|
|
2990
|
+
else existingTickets.push(candidate);
|
|
2718
2991
|
}
|
|
2719
2992
|
const postState = new ProjectState({
|
|
2720
|
-
tickets:
|
|
2721
|
-
issues:
|
|
2993
|
+
tickets: existingTickets,
|
|
2994
|
+
issues: [...state.issues],
|
|
2722
2995
|
notes: [...state.notes],
|
|
2723
2996
|
roadmap: state.roadmap,
|
|
2724
2997
|
config: state.config,
|
|
@@ -2731,389 +3004,281 @@ function validatePostWriteIssueState(candidate, state, isCreate) {
|
|
|
2731
3004
|
throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
|
|
2732
3005
|
}
|
|
2733
3006
|
}
|
|
2734
|
-
async function
|
|
2735
|
-
if (!
|
|
3007
|
+
async function handleTicketCreate(args, format, root) {
|
|
3008
|
+
if (!TICKET_TYPES.includes(args.type)) {
|
|
2736
3009
|
throw new CliValidationError(
|
|
2737
3010
|
"invalid_input",
|
|
2738
|
-
`Unknown
|
|
3011
|
+
`Unknown ticket type "${args.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
2739
3012
|
);
|
|
2740
3013
|
}
|
|
2741
|
-
let
|
|
3014
|
+
let createdTicket;
|
|
2742
3015
|
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2743
|
-
|
|
2744
|
-
|
|
3016
|
+
validatePhase(args.phase, { state });
|
|
3017
|
+
if (args.blockedBy.length > 0) {
|
|
3018
|
+
validateBlockedBy(args.blockedBy, "", state);
|
|
2745
3019
|
}
|
|
2746
|
-
if (args.
|
|
2747
|
-
|
|
3020
|
+
if (args.parentTicket) {
|
|
3021
|
+
validateParentTicket(args.parentTicket, "", state);
|
|
2748
3022
|
}
|
|
2749
|
-
const id =
|
|
2750
|
-
const
|
|
3023
|
+
const id = nextTicketID(state.tickets);
|
|
3024
|
+
const order = nextOrder(args.phase, state);
|
|
3025
|
+
const ticket = {
|
|
2751
3026
|
id,
|
|
2752
3027
|
title: args.title,
|
|
3028
|
+
description: args.description,
|
|
3029
|
+
type: args.type,
|
|
2753
3030
|
status: "open",
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
resolvedDate: null,
|
|
2761
|
-
relatedTickets: args.relatedTickets,
|
|
2762
|
-
phase: args.phase ?? null
|
|
2763
|
-
};
|
|
2764
|
-
validatePostWriteIssueState(issue, state, true);
|
|
2765
|
-
await writeIssueUnlocked(issue, root);
|
|
2766
|
-
createdIssue = issue;
|
|
2767
|
-
});
|
|
2768
|
-
if (!createdIssue) throw new Error("Issue not created");
|
|
2769
|
-
if (format === "json") {
|
|
2770
|
-
return { output: JSON.stringify(successEnvelope(createdIssue), null, 2) };
|
|
2771
|
-
}
|
|
2772
|
-
return { output: `Created issue ${createdIssue.id}: ${createdIssue.title}` };
|
|
2773
|
-
}
|
|
2774
|
-
async function handleIssueUpdate(id, updates, format, root) {
|
|
2775
|
-
if (updates.status && !ISSUE_STATUSES.includes(updates.status)) {
|
|
2776
|
-
throw new CliValidationError(
|
|
2777
|
-
"invalid_input",
|
|
2778
|
-
`Unknown issue status "${updates.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
|
|
2779
|
-
);
|
|
2780
|
-
}
|
|
2781
|
-
if (updates.severity && !ISSUE_SEVERITIES.includes(updates.severity)) {
|
|
2782
|
-
throw new CliValidationError(
|
|
2783
|
-
"invalid_input",
|
|
2784
|
-
`Unknown issue severity "${updates.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
|
|
2785
|
-
);
|
|
2786
|
-
}
|
|
2787
|
-
let updatedIssue;
|
|
2788
|
-
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
2789
|
-
const existing = state.issueByID(id);
|
|
2790
|
-
if (!existing) {
|
|
2791
|
-
throw new CliValidationError("not_found", `Issue ${id} not found`);
|
|
2792
|
-
}
|
|
2793
|
-
if (updates.phase !== void 0 && updates.phase !== null) {
|
|
2794
|
-
if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
|
|
2795
|
-
throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
if (updates.relatedTickets) {
|
|
2799
|
-
validateRelatedTickets(updates.relatedTickets, state);
|
|
2800
|
-
}
|
|
2801
|
-
const statusChanges = {};
|
|
2802
|
-
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
2803
|
-
statusChanges.status = updates.status;
|
|
2804
|
-
if (updates.status === "resolved" && existing.status !== "resolved") {
|
|
2805
|
-
statusChanges.resolvedDate = todayISO();
|
|
2806
|
-
} else if (updates.status !== "resolved" && existing.status === "resolved") {
|
|
2807
|
-
statusChanges.resolvedDate = null;
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
const issue = {
|
|
2811
|
-
...existing,
|
|
2812
|
-
...updates.title !== void 0 && { title: updates.title },
|
|
2813
|
-
...updates.severity !== void 0 && { severity: updates.severity },
|
|
2814
|
-
...updates.impact !== void 0 && { impact: updates.impact },
|
|
2815
|
-
...updates.resolution !== void 0 && { resolution: updates.resolution },
|
|
2816
|
-
...updates.components !== void 0 && { components: updates.components },
|
|
2817
|
-
...updates.relatedTickets !== void 0 && { relatedTickets: updates.relatedTickets },
|
|
2818
|
-
...updates.location !== void 0 && { location: updates.location },
|
|
2819
|
-
...updates.order !== void 0 && { order: updates.order },
|
|
2820
|
-
...updates.phase !== void 0 && { phase: updates.phase },
|
|
2821
|
-
...statusChanges
|
|
3031
|
+
phase: args.phase,
|
|
3032
|
+
order,
|
|
3033
|
+
createdDate: todayISO(),
|
|
3034
|
+
completedDate: null,
|
|
3035
|
+
blockedBy: args.blockedBy,
|
|
3036
|
+
parentTicket: args.parentTicket ?? void 0
|
|
2822
3037
|
};
|
|
2823
|
-
|
|
2824
|
-
await
|
|
2825
|
-
|
|
3038
|
+
validatePostWriteState(ticket, state, true);
|
|
3039
|
+
await writeTicketUnlocked(ticket, root);
|
|
3040
|
+
createdTicket = ticket;
|
|
2826
3041
|
});
|
|
2827
|
-
if (!
|
|
3042
|
+
if (!createdTicket) throw new Error("Ticket not created");
|
|
2828
3043
|
if (format === "json") {
|
|
2829
|
-
return { output: JSON.stringify(successEnvelope(
|
|
2830
|
-
}
|
|
2831
|
-
return { output: `Updated issue ${updatedIssue.id}: ${updatedIssue.title}` };
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
// src/core/snapshot.ts
|
|
2835
|
-
import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
|
|
2836
|
-
import { existsSync as existsSync5 } from "fs";
|
|
2837
|
-
import { join as join5, resolve as resolve5 } from "path";
|
|
2838
|
-
import { z as z7 } from "zod";
|
|
2839
|
-
var LoadWarningSchema = z7.object({
|
|
2840
|
-
type: z7.string(),
|
|
2841
|
-
file: z7.string(),
|
|
2842
|
-
message: z7.string()
|
|
2843
|
-
});
|
|
2844
|
-
var SnapshotV1Schema = z7.object({
|
|
2845
|
-
version: z7.literal(1),
|
|
2846
|
-
createdAt: z7.string().datetime({ offset: true }),
|
|
2847
|
-
project: z7.string(),
|
|
2848
|
-
config: ConfigSchema,
|
|
2849
|
-
roadmap: RoadmapSchema,
|
|
2850
|
-
tickets: z7.array(TicketSchema),
|
|
2851
|
-
issues: z7.array(IssueSchema),
|
|
2852
|
-
notes: z7.array(NoteSchema).optional().default([]),
|
|
2853
|
-
handoverFilenames: z7.array(z7.string()).optional().default([]),
|
|
2854
|
-
warnings: z7.array(LoadWarningSchema).optional()
|
|
2855
|
-
});
|
|
2856
|
-
var MAX_SNAPSHOTS = 20;
|
|
2857
|
-
async function saveSnapshot(root, loadResult) {
|
|
2858
|
-
const absRoot = resolve5(root);
|
|
2859
|
-
const snapshotsDir = join5(absRoot, ".story", "snapshots");
|
|
2860
|
-
await mkdir3(snapshotsDir, { recursive: true });
|
|
2861
|
-
const { state, warnings } = loadResult;
|
|
2862
|
-
const now = /* @__PURE__ */ new Date();
|
|
2863
|
-
const filename = formatSnapshotFilename(now);
|
|
2864
|
-
const snapshot = {
|
|
2865
|
-
version: 1,
|
|
2866
|
-
createdAt: now.toISOString(),
|
|
2867
|
-
project: state.config.project,
|
|
2868
|
-
config: state.config,
|
|
2869
|
-
roadmap: state.roadmap,
|
|
2870
|
-
tickets: [...state.tickets],
|
|
2871
|
-
issues: [...state.issues],
|
|
2872
|
-
notes: [...state.notes],
|
|
2873
|
-
handoverFilenames: [...state.handoverFilenames],
|
|
2874
|
-
...warnings.length > 0 ? {
|
|
2875
|
-
warnings: warnings.map((w) => ({
|
|
2876
|
-
type: w.type,
|
|
2877
|
-
file: w.file,
|
|
2878
|
-
message: w.message
|
|
2879
|
-
}))
|
|
2880
|
-
} : {}
|
|
2881
|
-
};
|
|
2882
|
-
const json = JSON.stringify(snapshot, null, 2) + "\n";
|
|
2883
|
-
const targetPath = join5(snapshotsDir, filename);
|
|
2884
|
-
const wrapDir = join5(absRoot, ".story");
|
|
2885
|
-
await guardPath(targetPath, wrapDir);
|
|
2886
|
-
await atomicWrite(targetPath, json);
|
|
2887
|
-
const pruned = await pruneSnapshots(snapshotsDir);
|
|
2888
|
-
const entries = await listSnapshotFiles(snapshotsDir);
|
|
2889
|
-
return { filename, retained: entries.length, pruned };
|
|
2890
|
-
}
|
|
2891
|
-
async function loadLatestSnapshot(root) {
|
|
2892
|
-
const snapshotsDir = join5(resolve5(root), ".story", "snapshots");
|
|
2893
|
-
if (!existsSync5(snapshotsDir)) return null;
|
|
2894
|
-
const files = await listSnapshotFiles(snapshotsDir);
|
|
2895
|
-
if (files.length === 0) return null;
|
|
2896
|
-
for (const filename of files) {
|
|
2897
|
-
try {
|
|
2898
|
-
const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
|
|
2899
|
-
const parsed = JSON.parse(content);
|
|
2900
|
-
const snapshot = SnapshotV1Schema.parse(parsed);
|
|
2901
|
-
return { snapshot, filename };
|
|
2902
|
-
} catch {
|
|
2903
|
-
continue;
|
|
2904
|
-
}
|
|
3044
|
+
return { output: JSON.stringify(successEnvelope(createdTicket), null, 2) };
|
|
2905
3045
|
}
|
|
2906
|
-
return
|
|
3046
|
+
return { output: `Created ticket ${createdTicket.id}: ${createdTicket.title}` };
|
|
2907
3047
|
}
|
|
2908
|
-
function
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
const ticketsDescriptionChanged = [];
|
|
2915
|
-
for (const [id, cur] of curTickets) {
|
|
2916
|
-
const snap = snapTickets.get(id);
|
|
2917
|
-
if (!snap) {
|
|
2918
|
-
ticketsAdded.push({ id, title: cur.title });
|
|
2919
|
-
} else {
|
|
2920
|
-
if (snap.status !== cur.status) {
|
|
2921
|
-
ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2922
|
-
}
|
|
2923
|
-
if (snap.description !== cur.description) {
|
|
2924
|
-
ticketsDescriptionChanged.push({ id, title: cur.title });
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
3048
|
+
async function handleTicketUpdate(id, updates, format, root) {
|
|
3049
|
+
if (updates.status && !TICKET_STATUSES.includes(updates.status)) {
|
|
3050
|
+
throw new CliValidationError(
|
|
3051
|
+
"invalid_input",
|
|
3052
|
+
`Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
|
|
3053
|
+
);
|
|
2927
3054
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3055
|
+
if (updates.type !== void 0 && !TICKET_TYPES.includes(updates.type)) {
|
|
3056
|
+
throw new CliValidationError(
|
|
3057
|
+
"invalid_input",
|
|
3058
|
+
`Unknown ticket type "${updates.type}": must be one of ${TICKET_TYPES.join(", ")}`
|
|
3059
|
+
);
|
|
2932
3060
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
const issuesImpactChanged = [];
|
|
2939
|
-
for (const [id, cur] of curIssues) {
|
|
2940
|
-
const snap = snapIssues.get(id);
|
|
2941
|
-
if (!snap) {
|
|
2942
|
-
issuesAdded.push({ id, title: cur.title });
|
|
2943
|
-
} else {
|
|
2944
|
-
if (snap.status !== cur.status) {
|
|
2945
|
-
if (cur.status === "resolved") {
|
|
2946
|
-
issuesResolved.push({ id, title: cur.title });
|
|
2947
|
-
} else {
|
|
2948
|
-
issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
if (snap.impact !== cur.impact) {
|
|
2952
|
-
issuesImpactChanged.push({ id, title: cur.title });
|
|
2953
|
-
}
|
|
3061
|
+
let updatedTicket;
|
|
3062
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3063
|
+
const existing = state.ticketByID(id);
|
|
3064
|
+
if (!existing) {
|
|
3065
|
+
throw new CliValidationError("not_found", `Ticket ${id} not found`);
|
|
2954
3066
|
}
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
snapshotState.roadmap.blockers.map((b) => [b.name, b])
|
|
2958
|
-
);
|
|
2959
|
-
const curBlockers = new Map(
|
|
2960
|
-
currentState.roadmap.blockers.map((b) => [b.name, b])
|
|
2961
|
-
);
|
|
2962
|
-
const blockersAdded = [];
|
|
2963
|
-
const blockersCleared = [];
|
|
2964
|
-
for (const [name, cur] of curBlockers) {
|
|
2965
|
-
const snap = snapBlockers.get(name);
|
|
2966
|
-
if (!snap) {
|
|
2967
|
-
blockersAdded.push(name);
|
|
2968
|
-
} else if (!isBlockerCleared(snap) && isBlockerCleared(cur)) {
|
|
2969
|
-
blockersCleared.push(name);
|
|
3067
|
+
if (updates.phase !== void 0) {
|
|
3068
|
+
validatePhase(updates.phase, { state });
|
|
2970
3069
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
const curPhases = currentState.roadmap.phases;
|
|
2974
|
-
const snapPhaseMap = new Map(snapPhases.map((p) => [p.id, p]));
|
|
2975
|
-
const curPhaseMap = new Map(curPhases.map((p) => [p.id, p]));
|
|
2976
|
-
const phasesAdded = [];
|
|
2977
|
-
const phasesRemoved = [];
|
|
2978
|
-
const phasesStatusChanged = [];
|
|
2979
|
-
for (const [id, curPhase] of curPhaseMap) {
|
|
2980
|
-
const snapPhase = snapPhaseMap.get(id);
|
|
2981
|
-
if (!snapPhase) {
|
|
2982
|
-
phasesAdded.push({ id, name: curPhase.name });
|
|
2983
|
-
} else {
|
|
2984
|
-
const snapStatus = snapshotState.phaseStatus(id);
|
|
2985
|
-
const curStatus = currentState.phaseStatus(id);
|
|
2986
|
-
if (snapStatus !== curStatus) {
|
|
2987
|
-
phasesStatusChanged.push({
|
|
2988
|
-
id,
|
|
2989
|
-
name: curPhase.name,
|
|
2990
|
-
from: snapStatus,
|
|
2991
|
-
to: curStatus
|
|
2992
|
-
});
|
|
2993
|
-
}
|
|
3070
|
+
if (updates.blockedBy) {
|
|
3071
|
+
validateBlockedBy(updates.blockedBy, id, state);
|
|
2994
3072
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
if (!curPhaseMap.has(id)) {
|
|
2998
|
-
phasesRemoved.push({ id, name: snapPhase.name });
|
|
3073
|
+
if (updates.parentTicket) {
|
|
3074
|
+
validateParentTicket(updates.parentTicket, id, state);
|
|
2999
3075
|
}
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const snap = snapNotes.get(id);
|
|
3008
|
-
if (!snap) {
|
|
3009
|
-
notesAdded.push({ id, title: cur.title });
|
|
3010
|
-
} else {
|
|
3011
|
-
const changedFields = [];
|
|
3012
|
-
if (snap.title !== cur.title) changedFields.push("title");
|
|
3013
|
-
if (snap.content !== cur.content) changedFields.push("content");
|
|
3014
|
-
if (JSON.stringify([...snap.tags].sort()) !== JSON.stringify([...cur.tags].sort())) changedFields.push("tags");
|
|
3015
|
-
if (snap.status !== cur.status) changedFields.push("status");
|
|
3016
|
-
if (changedFields.length > 0) {
|
|
3017
|
-
notesUpdated.push({ id, title: cur.title, changedFields });
|
|
3076
|
+
const statusChanges = {};
|
|
3077
|
+
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
3078
|
+
statusChanges.status = updates.status;
|
|
3079
|
+
if (updates.status === "complete" && existing.status !== "complete") {
|
|
3080
|
+
statusChanges.completedDate = todayISO();
|
|
3081
|
+
} else if (updates.status !== "complete" && existing.status === "complete") {
|
|
3082
|
+
statusChanges.completedDate = null;
|
|
3018
3083
|
}
|
|
3019
3084
|
}
|
|
3085
|
+
const ticket = {
|
|
3086
|
+
...existing,
|
|
3087
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
3088
|
+
...updates.type !== void 0 && { type: updates.type },
|
|
3089
|
+
...updates.description !== void 0 && { description: updates.description },
|
|
3090
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
3091
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
3092
|
+
...updates.blockedBy !== void 0 && { blockedBy: updates.blockedBy },
|
|
3093
|
+
...updates.parentTicket !== void 0 && { parentTicket: updates.parentTicket },
|
|
3094
|
+
...statusChanges
|
|
3095
|
+
};
|
|
3096
|
+
validatePostWriteState(ticket, state, false);
|
|
3097
|
+
await writeTicketUnlocked(ticket, root);
|
|
3098
|
+
updatedTicket = ticket;
|
|
3099
|
+
});
|
|
3100
|
+
if (!updatedTicket) throw new Error("Ticket not updated");
|
|
3101
|
+
if (format === "json") {
|
|
3102
|
+
return { output: JSON.stringify(successEnvelope(updatedTicket), null, 2) };
|
|
3020
3103
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3104
|
+
return { output: `Updated ticket ${updatedTicket.id}: ${updatedTicket.title}` };
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/cli/commands/issue.ts
|
|
3108
|
+
init_esm_shims();
|
|
3109
|
+
init_project_state();
|
|
3110
|
+
init_project_loader();
|
|
3111
|
+
init_types();
|
|
3112
|
+
function handleIssueList(filters, ctx) {
|
|
3113
|
+
let issues = [...ctx.state.issues];
|
|
3114
|
+
if (filters.status) {
|
|
3115
|
+
if (!ISSUE_STATUSES.includes(filters.status)) {
|
|
3116
|
+
throw new CliValidationError(
|
|
3117
|
+
"invalid_input",
|
|
3118
|
+
`Unknown issue status "${filters.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
|
|
3119
|
+
);
|
|
3024
3120
|
}
|
|
3121
|
+
issues = issues.filter((i) => i.status === filters.status);
|
|
3025
3122
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3123
|
+
if (filters.severity) {
|
|
3124
|
+
if (!ISSUE_SEVERITIES.includes(filters.severity)) {
|
|
3125
|
+
throw new CliValidationError(
|
|
3126
|
+
"invalid_input",
|
|
3127
|
+
`Unknown issue severity "${filters.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
issues = issues.filter((i) => i.severity === filters.severity);
|
|
3032
3131
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3132
|
+
if (filters.component) {
|
|
3133
|
+
issues = issues.filter((i) => i.components.includes(filters.component));
|
|
3035
3134
|
}
|
|
3036
|
-
return {
|
|
3037
|
-
tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
|
|
3038
|
-
issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
|
|
3039
|
-
blockers: { added: blockersAdded, cleared: blockersCleared },
|
|
3040
|
-
phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
|
|
3041
|
-
notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated },
|
|
3042
|
-
handovers: { added: handoversAdded, removed: handoversRemoved }
|
|
3043
|
-
};
|
|
3135
|
+
return { output: formatIssueList(issues, ctx.format) };
|
|
3044
3136
|
}
|
|
3045
|
-
function
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3048
|
-
const highSeverityIssues = currentState.issues.filter(
|
|
3049
|
-
(i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
|
|
3050
|
-
).map((i) => ({ id: i.id, title: i.title, severity: i.severity }));
|
|
3051
|
-
if (!snapshotInfo) {
|
|
3137
|
+
function handleIssueGet(id, ctx) {
|
|
3138
|
+
const issue = ctx.state.issueByID(id);
|
|
3139
|
+
if (!issue) {
|
|
3052
3140
|
return {
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
nextTicket: nextTicketAction,
|
|
3057
|
-
highSeverityIssues,
|
|
3058
|
-
recentlyClearedBlockers: []
|
|
3059
|
-
},
|
|
3060
|
-
partial: false
|
|
3141
|
+
output: formatError("not_found", `Issue ${id} not found`, ctx.format),
|
|
3142
|
+
exitCode: ExitCode.USER_ERROR,
|
|
3143
|
+
errorCode: "not_found"
|
|
3061
3144
|
};
|
|
3062
3145
|
}
|
|
3063
|
-
|
|
3064
|
-
const snapshotState = new ProjectState({
|
|
3065
|
-
tickets: snapshot.tickets,
|
|
3066
|
-
issues: snapshot.issues,
|
|
3067
|
-
notes: snapshot.notes ?? [],
|
|
3068
|
-
roadmap: snapshot.roadmap,
|
|
3069
|
-
config: snapshot.config,
|
|
3070
|
-
handoverFilenames: snapshot.handoverFilenames ?? []
|
|
3071
|
-
});
|
|
3072
|
-
const changes = diffStates(snapshotState, currentState);
|
|
3073
|
-
const recentlyClearedBlockers = changes.blockers.cleared;
|
|
3074
|
-
return {
|
|
3075
|
-
snapshot: { filename, createdAt: snapshot.createdAt },
|
|
3076
|
-
changes,
|
|
3077
|
-
suggestedActions: {
|
|
3078
|
-
nextTicket: nextTicketAction,
|
|
3079
|
-
highSeverityIssues,
|
|
3080
|
-
recentlyClearedBlockers
|
|
3081
|
-
},
|
|
3082
|
-
partial: (snapshot.warnings ?? []).length > 0
|
|
3083
|
-
};
|
|
3146
|
+
return { output: formatIssue(issue, ctx.format) };
|
|
3084
3147
|
}
|
|
3085
|
-
function
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3148
|
+
function validateRelatedTickets(ids, state) {
|
|
3149
|
+
for (const tid of ids) {
|
|
3150
|
+
if (!state.ticketByID(tid)) {
|
|
3151
|
+
throw new CliValidationError("invalid_input", `Related ticket ${tid} not found`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
function validatePostWriteIssueState(candidate, state, isCreate) {
|
|
3156
|
+
const existingIssues = [...state.issues];
|
|
3157
|
+
if (isCreate) {
|
|
3158
|
+
existingIssues.push(candidate);
|
|
3159
|
+
} else {
|
|
3160
|
+
const idx = existingIssues.findIndex((i) => i.id === candidate.id);
|
|
3161
|
+
if (idx >= 0) existingIssues[idx] = candidate;
|
|
3162
|
+
else existingIssues.push(candidate);
|
|
3163
|
+
}
|
|
3164
|
+
const postState = new ProjectState({
|
|
3165
|
+
tickets: [...state.tickets],
|
|
3166
|
+
issues: existingIssues,
|
|
3167
|
+
notes: [...state.notes],
|
|
3168
|
+
roadmap: state.roadmap,
|
|
3169
|
+
config: state.config,
|
|
3170
|
+
handoverFilenames: [...state.handoverFilenames]
|
|
3171
|
+
});
|
|
3172
|
+
const result = validateProject(postState);
|
|
3173
|
+
if (!result.valid) {
|
|
3174
|
+
const errors = result.findings.filter((f) => f.level === "error");
|
|
3175
|
+
const msg = errors.map((f) => f.message).join("; ");
|
|
3176
|
+
throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
|
|
3177
|
+
}
|
|
3094
3178
|
}
|
|
3095
|
-
async function
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3179
|
+
async function handleIssueCreate(args, format, root) {
|
|
3180
|
+
if (!ISSUE_SEVERITIES.includes(args.severity)) {
|
|
3181
|
+
throw new CliValidationError(
|
|
3182
|
+
"invalid_input",
|
|
3183
|
+
`Unknown issue severity "${args.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
let createdIssue;
|
|
3187
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3188
|
+
if (args.phase && !state.roadmap.phases.some((p) => p.id === args.phase)) {
|
|
3189
|
+
throw new CliValidationError("invalid_input", `Phase "${args.phase}" not found in roadmap`);
|
|
3190
|
+
}
|
|
3191
|
+
if (args.relatedTickets.length > 0) {
|
|
3192
|
+
validateRelatedTickets(args.relatedTickets, state);
|
|
3193
|
+
}
|
|
3194
|
+
const id = nextIssueID(state.issues);
|
|
3195
|
+
const issue = {
|
|
3196
|
+
id,
|
|
3197
|
+
title: args.title,
|
|
3198
|
+
status: "open",
|
|
3199
|
+
severity: args.severity,
|
|
3200
|
+
components: args.components,
|
|
3201
|
+
impact: args.impact,
|
|
3202
|
+
resolution: null,
|
|
3203
|
+
location: args.location,
|
|
3204
|
+
discoveredDate: todayISO(),
|
|
3205
|
+
resolvedDate: null,
|
|
3206
|
+
relatedTickets: args.relatedTickets,
|
|
3207
|
+
phase: args.phase ?? null
|
|
3208
|
+
};
|
|
3209
|
+
validatePostWriteIssueState(issue, state, true);
|
|
3210
|
+
await writeIssueUnlocked(issue, root);
|
|
3211
|
+
createdIssue = issue;
|
|
3212
|
+
});
|
|
3213
|
+
if (!createdIssue) throw new Error("Issue not created");
|
|
3214
|
+
if (format === "json") {
|
|
3215
|
+
return { output: JSON.stringify(successEnvelope(createdIssue), null, 2) };
|
|
3101
3216
|
}
|
|
3217
|
+
return { output: `Created issue ${createdIssue.id}: ${createdIssue.title}` };
|
|
3102
3218
|
}
|
|
3103
|
-
async function
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3219
|
+
async function handleIssueUpdate(id, updates, format, root) {
|
|
3220
|
+
if (updates.status && !ISSUE_STATUSES.includes(updates.status)) {
|
|
3221
|
+
throw new CliValidationError(
|
|
3222
|
+
"invalid_input",
|
|
3223
|
+
`Unknown issue status "${updates.status}": must be one of ${ISSUE_STATUSES.join(", ")}`
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
if (updates.severity && !ISSUE_SEVERITIES.includes(updates.severity)) {
|
|
3227
|
+
throw new CliValidationError(
|
|
3228
|
+
"invalid_input",
|
|
3229
|
+
`Unknown issue severity "${updates.severity}": must be one of ${ISSUE_SEVERITIES.join(", ")}`
|
|
3230
|
+
);
|
|
3231
|
+
}
|
|
3232
|
+
let updatedIssue;
|
|
3233
|
+
await withProjectLock(root, { strict: true }, async ({ state }) => {
|
|
3234
|
+
const existing = state.issueByID(id);
|
|
3235
|
+
if (!existing) {
|
|
3236
|
+
throw new CliValidationError("not_found", `Issue ${id} not found`);
|
|
3237
|
+
}
|
|
3238
|
+
if (updates.phase !== void 0 && updates.phase !== null) {
|
|
3239
|
+
if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
|
|
3240
|
+
throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (updates.relatedTickets) {
|
|
3244
|
+
validateRelatedTickets(updates.relatedTickets, state);
|
|
3245
|
+
}
|
|
3246
|
+
const statusChanges = {};
|
|
3247
|
+
if (updates.status !== void 0 && updates.status !== existing.status) {
|
|
3248
|
+
statusChanges.status = updates.status;
|
|
3249
|
+
if (updates.status === "resolved" && existing.status !== "resolved") {
|
|
3250
|
+
statusChanges.resolvedDate = todayISO();
|
|
3251
|
+
} else if (updates.status !== "resolved" && existing.status === "resolved") {
|
|
3252
|
+
statusChanges.resolvedDate = null;
|
|
3253
|
+
}
|
|
3111
3254
|
}
|
|
3255
|
+
const issue = {
|
|
3256
|
+
...existing,
|
|
3257
|
+
...updates.title !== void 0 && { title: updates.title },
|
|
3258
|
+
...updates.severity !== void 0 && { severity: updates.severity },
|
|
3259
|
+
...updates.impact !== void 0 && { impact: updates.impact },
|
|
3260
|
+
...updates.resolution !== void 0 && { resolution: updates.resolution },
|
|
3261
|
+
...updates.components !== void 0 && { components: updates.components },
|
|
3262
|
+
...updates.relatedTickets !== void 0 && { relatedTickets: updates.relatedTickets },
|
|
3263
|
+
...updates.location !== void 0 && { location: updates.location },
|
|
3264
|
+
...updates.order !== void 0 && { order: updates.order },
|
|
3265
|
+
...updates.phase !== void 0 && { phase: updates.phase },
|
|
3266
|
+
...statusChanges
|
|
3267
|
+
};
|
|
3268
|
+
validatePostWriteIssueState(issue, state, false);
|
|
3269
|
+
await writeIssueUnlocked(issue, root);
|
|
3270
|
+
updatedIssue = issue;
|
|
3271
|
+
});
|
|
3272
|
+
if (!updatedIssue) throw new Error("Issue not updated");
|
|
3273
|
+
if (format === "json") {
|
|
3274
|
+
return { output: JSON.stringify(successEnvelope(updatedIssue), null, 2) };
|
|
3112
3275
|
}
|
|
3113
|
-
return
|
|
3276
|
+
return { output: `Updated issue ${updatedIssue.id}: ${updatedIssue.title}` };
|
|
3114
3277
|
}
|
|
3115
3278
|
|
|
3116
3279
|
// src/cli/commands/recap.ts
|
|
3280
|
+
init_esm_shims();
|
|
3281
|
+
init_snapshot();
|
|
3117
3282
|
async function handleRecap(ctx) {
|
|
3118
3283
|
const snapshotInfo = await loadLatestSnapshot(ctx.root);
|
|
3119
3284
|
const recap = buildRecap(ctx.state, snapshotInfo);
|
|
@@ -3121,6 +3286,9 @@ async function handleRecap(ctx) {
|
|
|
3121
3286
|
}
|
|
3122
3287
|
|
|
3123
3288
|
// src/cli/commands/note.ts
|
|
3289
|
+
init_esm_shims();
|
|
3290
|
+
init_project_loader();
|
|
3291
|
+
init_types();
|
|
3124
3292
|
function handleNoteList(filters, ctx) {
|
|
3125
3293
|
let notes = [...ctx.state.notes];
|
|
3126
3294
|
if (filters.status) {
|
|
@@ -3222,7 +3390,12 @@ async function handleNoteUpdate(id, updates, format, root) {
|
|
|
3222
3390
|
return { output: formatNoteUpdateResult(updatedNote, format) };
|
|
3223
3391
|
}
|
|
3224
3392
|
|
|
3393
|
+
// src/cli/commands/recommend.ts
|
|
3394
|
+
init_esm_shims();
|
|
3395
|
+
|
|
3225
3396
|
// src/core/recommend.ts
|
|
3397
|
+
init_esm_shims();
|
|
3398
|
+
init_queries();
|
|
3226
3399
|
var SEVERITY_RANK = {
|
|
3227
3400
|
critical: 4,
|
|
3228
3401
|
high: 3,
|
|
@@ -3460,6 +3633,9 @@ function handleRecommend(ctx, count) {
|
|
|
3460
3633
|
}
|
|
3461
3634
|
|
|
3462
3635
|
// src/cli/commands/snapshot.ts
|
|
3636
|
+
init_esm_shims();
|
|
3637
|
+
init_snapshot();
|
|
3638
|
+
init_project_loader();
|
|
3463
3639
|
async function handleSnapshot(root, format, options) {
|
|
3464
3640
|
let result;
|
|
3465
3641
|
await withProjectLock(root, { strict: false }, async (loadResult) => {
|
|
@@ -3475,6 +3651,7 @@ async function handleSnapshot(root, format, options) {
|
|
|
3475
3651
|
}
|
|
3476
3652
|
|
|
3477
3653
|
// src/cli/commands/export.ts
|
|
3654
|
+
init_esm_shims();
|
|
3478
3655
|
function handleExport(ctx, mode, phaseId) {
|
|
3479
3656
|
if (mode === "phase") {
|
|
3480
3657
|
if (!phaseId) {
|
|
@@ -3489,6 +3666,8 @@ function handleExport(ctx, mode, phaseId) {
|
|
|
3489
3666
|
}
|
|
3490
3667
|
|
|
3491
3668
|
// src/cli/commands/selftest.ts
|
|
3669
|
+
init_esm_shims();
|
|
3670
|
+
init_project_loader();
|
|
3492
3671
|
async function handleSelftest(root, format, failAfter) {
|
|
3493
3672
|
const results = [];
|
|
3494
3673
|
const createdIds = [];
|
|
@@ -3660,76 +3839,1516 @@ async function handleSelftest(root, format, failAfter) {
|
|
|
3660
3839
|
}
|
|
3661
3840
|
if (noteId) {
|
|
3662
3841
|
try {
|
|
3663
|
-
const { state } = await loadProject(root);
|
|
3664
|
-
const found = state.noteByID(noteId);
|
|
3665
|
-
if (!found) throw new Error(`${noteId} not found after create`);
|
|
3666
|
-
record("note", "get", true, `Found ${noteId}`);
|
|
3667
|
-
} catch (err) {
|
|
3668
|
-
record("note", "get", false, errMsg(err));
|
|
3842
|
+
const { state } = await loadProject(root);
|
|
3843
|
+
const found = state.noteByID(noteId);
|
|
3844
|
+
if (!found) throw new Error(`${noteId} not found after create`);
|
|
3845
|
+
record("note", "get", true, `Found ${noteId}`);
|
|
3846
|
+
} catch (err) {
|
|
3847
|
+
record("note", "get", false, errMsg(err));
|
|
3848
|
+
}
|
|
3849
|
+
try {
|
|
3850
|
+
const { state } = await loadProject(root);
|
|
3851
|
+
const existing = state.noteByID(noteId);
|
|
3852
|
+
if (!existing) throw new Error(`${noteId} not found for update`);
|
|
3853
|
+
const updated = { ...existing, status: "archived", updatedDate: todayISO() };
|
|
3854
|
+
await writeNote(updated, root);
|
|
3855
|
+
record("note", "update", true, `Updated ${noteId} status \u2192 archived`);
|
|
3856
|
+
} catch (err) {
|
|
3857
|
+
record("note", "update", false, errMsg(err));
|
|
3858
|
+
}
|
|
3859
|
+
try {
|
|
3860
|
+
const { state } = await loadProject(root);
|
|
3861
|
+
const found = state.noteByID(noteId);
|
|
3862
|
+
if (!found) throw new Error(`${noteId} not found for verify`);
|
|
3863
|
+
if (found.status !== "archived") throw new Error(`Expected archived, got ${found.status}`);
|
|
3864
|
+
record("note", "verify update", true, `Verified ${noteId} status = archived`);
|
|
3865
|
+
} catch (err) {
|
|
3866
|
+
record("note", "verify update", false, errMsg(err));
|
|
3867
|
+
}
|
|
3868
|
+
try {
|
|
3869
|
+
await deleteNote(noteId, root);
|
|
3870
|
+
createdIds.splice(createdIds.findIndex((c) => c.id === noteId), 1);
|
|
3871
|
+
record("note", "delete", true, `Deleted ${noteId}`);
|
|
3872
|
+
} catch (err) {
|
|
3873
|
+
record("note", "delete", false, errMsg(err));
|
|
3874
|
+
}
|
|
3875
|
+
try {
|
|
3876
|
+
const { state } = await loadProject(root);
|
|
3877
|
+
const found = state.noteByID(noteId);
|
|
3878
|
+
if (found) throw new Error(`${noteId} still exists after delete`);
|
|
3879
|
+
record("note", "verify delete", true, `Confirmed ${noteId} absent`);
|
|
3880
|
+
} catch (err) {
|
|
3881
|
+
record("note", "verify delete", false, errMsg(err));
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
} finally {
|
|
3885
|
+
for (const { type, id } of createdIds.reverse()) {
|
|
3886
|
+
try {
|
|
3887
|
+
if (type === "ticket") await deleteTicket(id, root, { force: true });
|
|
3888
|
+
else if (type === "issue") await deleteIssue(id, root);
|
|
3889
|
+
else await deleteNote(id, root);
|
|
3890
|
+
} catch (err) {
|
|
3891
|
+
cleanupErrors.push(`Failed to delete ${type} ${id}: ${errMsg(err)}`);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
const passed = results.filter((r) => r.passed).length;
|
|
3896
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
3897
|
+
const result = {
|
|
3898
|
+
passed,
|
|
3899
|
+
failed,
|
|
3900
|
+
total: results.length,
|
|
3901
|
+
results,
|
|
3902
|
+
cleanupErrors
|
|
3903
|
+
};
|
|
3904
|
+
return { output: formatSelftestResult(result, format) };
|
|
3905
|
+
}
|
|
3906
|
+
function errMsg(err) {
|
|
3907
|
+
return err instanceof Error ? err.message : String(err);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
// src/autonomous/guide.ts
|
|
3911
|
+
init_esm_shims();
|
|
3912
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync7 } from "fs";
|
|
3913
|
+
import { join as join7 } from "path";
|
|
3914
|
+
|
|
3915
|
+
// src/autonomous/session-types.ts
|
|
3916
|
+
init_esm_shims();
|
|
3917
|
+
import { realpathSync } from "fs";
|
|
3918
|
+
import { z as z8 } from "zod";
|
|
3919
|
+
function deriveWorkspaceId(projectRoot) {
|
|
3920
|
+
return realpathSync(projectRoot);
|
|
3921
|
+
}
|
|
3922
|
+
var WORKFLOW_STATES = [
|
|
3923
|
+
"INIT",
|
|
3924
|
+
"LOAD_CONTEXT",
|
|
3925
|
+
"PICK_TICKET",
|
|
3926
|
+
"PLAN",
|
|
3927
|
+
"PLAN_REVIEW",
|
|
3928
|
+
"IMPLEMENT",
|
|
3929
|
+
"CODE_REVIEW",
|
|
3930
|
+
"FINALIZE",
|
|
3931
|
+
"COMPACT",
|
|
3932
|
+
"HANDOVER",
|
|
3933
|
+
"COMPLETE",
|
|
3934
|
+
"SESSION_END"
|
|
3935
|
+
];
|
|
3936
|
+
var WorkflowStateSchema = z8.enum(WORKFLOW_STATES);
|
|
3937
|
+
var CURRENT_SESSION_SCHEMA_VERSION = 1;
|
|
3938
|
+
var SessionStateSchema = z8.object({
|
|
3939
|
+
schemaVersion: z8.literal(CURRENT_SESSION_SCHEMA_VERSION),
|
|
3940
|
+
sessionId: z8.string().uuid(),
|
|
3941
|
+
recipe: z8.string(),
|
|
3942
|
+
state: z8.string(),
|
|
3943
|
+
previousState: z8.string().optional(),
|
|
3944
|
+
revision: z8.number().int().min(0),
|
|
3945
|
+
status: z8.enum(["active", "completed", "superseded"]).default("active"),
|
|
3946
|
+
// Ticket in progress
|
|
3947
|
+
ticket: z8.object({
|
|
3948
|
+
id: z8.string(),
|
|
3949
|
+
title: z8.string(),
|
|
3950
|
+
risk: z8.string().optional(),
|
|
3951
|
+
realizedRisk: z8.string().optional(),
|
|
3952
|
+
claimed: z8.boolean().default(false)
|
|
3953
|
+
}).optional(),
|
|
3954
|
+
// Review tracking
|
|
3955
|
+
reviews: z8.object({
|
|
3956
|
+
plan: z8.array(z8.object({
|
|
3957
|
+
round: z8.number(),
|
|
3958
|
+
reviewer: z8.string(),
|
|
3959
|
+
verdict: z8.string(),
|
|
3960
|
+
findingCount: z8.number(),
|
|
3961
|
+
criticalCount: z8.number(),
|
|
3962
|
+
majorCount: z8.number(),
|
|
3963
|
+
suggestionCount: z8.number(),
|
|
3964
|
+
codexSessionId: z8.string().optional(),
|
|
3965
|
+
timestamp: z8.string()
|
|
3966
|
+
})).default([]),
|
|
3967
|
+
code: z8.array(z8.object({
|
|
3968
|
+
round: z8.number(),
|
|
3969
|
+
reviewer: z8.string(),
|
|
3970
|
+
verdict: z8.string(),
|
|
3971
|
+
findingCount: z8.number(),
|
|
3972
|
+
criticalCount: z8.number(),
|
|
3973
|
+
majorCount: z8.number(),
|
|
3974
|
+
suggestionCount: z8.number(),
|
|
3975
|
+
codexSessionId: z8.string().optional(),
|
|
3976
|
+
timestamp: z8.string()
|
|
3977
|
+
})).default([])
|
|
3978
|
+
}).default({ plan: [], code: [] }),
|
|
3979
|
+
// Completed tickets this session
|
|
3980
|
+
completedTickets: z8.array(z8.object({
|
|
3981
|
+
id: z8.string(),
|
|
3982
|
+
title: z8.string().optional(),
|
|
3983
|
+
commitHash: z8.string().optional(),
|
|
3984
|
+
risk: z8.string().optional()
|
|
3985
|
+
})).default([]),
|
|
3986
|
+
// FINALIZE checkpoint
|
|
3987
|
+
finalizeCheckpoint: z8.enum(["staged", "precommit_passed", "committed"]).nullable().default(null),
|
|
3988
|
+
// Git state
|
|
3989
|
+
git: z8.object({
|
|
3990
|
+
branch: z8.string().nullable().default(null),
|
|
3991
|
+
initHead: z8.string().optional(),
|
|
3992
|
+
mergeBase: z8.string().nullable().default(null),
|
|
3993
|
+
expectedHead: z8.string().optional(),
|
|
3994
|
+
baseline: z8.object({
|
|
3995
|
+
porcelain: z8.array(z8.string()).default([]),
|
|
3996
|
+
dirtyTrackedFiles: z8.record(z8.object({ blobHash: z8.string() })).default({}),
|
|
3997
|
+
untrackedPaths: z8.array(z8.string()).default([])
|
|
3998
|
+
}).optional()
|
|
3999
|
+
}).default({ branch: null, mergeBase: null }),
|
|
4000
|
+
// Lease
|
|
4001
|
+
lease: z8.object({
|
|
4002
|
+
workspaceId: z8.string().optional(),
|
|
4003
|
+
lastHeartbeat: z8.string(),
|
|
4004
|
+
expiresAt: z8.string()
|
|
4005
|
+
}),
|
|
4006
|
+
// Context pressure
|
|
4007
|
+
contextPressure: z8.object({
|
|
4008
|
+
level: z8.string().default("low"),
|
|
4009
|
+
guideCallCount: z8.number().default(0),
|
|
4010
|
+
ticketsCompleted: z8.number().default(0),
|
|
4011
|
+
compactionCount: z8.number().default(0),
|
|
4012
|
+
eventsLogBytes: z8.number().default(0)
|
|
4013
|
+
}).default({ level: "low", guideCallCount: 0, ticketsCompleted: 0, compactionCount: 0, eventsLogBytes: 0 }),
|
|
4014
|
+
// Pending project mutation (for crash recovery)
|
|
4015
|
+
pendingProjectMutation: z8.any().nullable().default(null),
|
|
4016
|
+
// COMPACT resume
|
|
4017
|
+
resumeFromRevision: z8.number().nullable().default(null),
|
|
4018
|
+
preCompactState: z8.string().nullable().default(null),
|
|
4019
|
+
// Session metadata
|
|
4020
|
+
waitingForRetry: z8.boolean().default(false),
|
|
4021
|
+
lastGuideCall: z8.string().optional(),
|
|
4022
|
+
startedAt: z8.string(),
|
|
4023
|
+
guideCallCount: z8.number().default(0),
|
|
4024
|
+
// Supersession tracking
|
|
4025
|
+
supersededBy: z8.string().optional(),
|
|
4026
|
+
supersededSession: z8.string().optional(),
|
|
4027
|
+
stealReason: z8.string().optional(),
|
|
4028
|
+
// Recipe overrides
|
|
4029
|
+
config: z8.object({
|
|
4030
|
+
maxTicketsPerSession: z8.number().default(3),
|
|
4031
|
+
compactThreshold: z8.string().default("high"),
|
|
4032
|
+
reviewBackends: z8.array(z8.string()).default(["codex", "agent"])
|
|
4033
|
+
}).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"] })
|
|
4034
|
+
}).passthrough();
|
|
4035
|
+
|
|
4036
|
+
// src/autonomous/session.ts
|
|
4037
|
+
init_esm_shims();
|
|
4038
|
+
import { randomUUID } from "crypto";
|
|
4039
|
+
import {
|
|
4040
|
+
mkdirSync,
|
|
4041
|
+
readdirSync,
|
|
4042
|
+
readFileSync,
|
|
4043
|
+
writeFileSync,
|
|
4044
|
+
renameSync,
|
|
4045
|
+
unlinkSync,
|
|
4046
|
+
existsSync as existsSync6,
|
|
4047
|
+
rmSync
|
|
4048
|
+
} from "fs";
|
|
4049
|
+
import { join as join6 } from "path";
|
|
4050
|
+
import lockfile2 from "proper-lockfile";
|
|
4051
|
+
var LEASE_DURATION_MS = 45 * 60 * 1e3;
|
|
4052
|
+
var SESSIONS_DIR = "sessions";
|
|
4053
|
+
function sessionsRoot(root) {
|
|
4054
|
+
return join6(root, ".story", SESSIONS_DIR);
|
|
4055
|
+
}
|
|
4056
|
+
function sessionDir(root, sessionId) {
|
|
4057
|
+
return join6(sessionsRoot(root), sessionId);
|
|
4058
|
+
}
|
|
4059
|
+
function statePath(dir) {
|
|
4060
|
+
return join6(dir, "state.json");
|
|
4061
|
+
}
|
|
4062
|
+
function eventsPath(dir) {
|
|
4063
|
+
return join6(dir, "events.log");
|
|
4064
|
+
}
|
|
4065
|
+
function createSession(root, recipe, workspaceId) {
|
|
4066
|
+
const id = randomUUID();
|
|
4067
|
+
const dir = sessionDir(root, id);
|
|
4068
|
+
mkdirSync(dir, { recursive: true });
|
|
4069
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4070
|
+
const state = {
|
|
4071
|
+
schemaVersion: CURRENT_SESSION_SCHEMA_VERSION,
|
|
4072
|
+
sessionId: id,
|
|
4073
|
+
recipe,
|
|
4074
|
+
state: "INIT",
|
|
4075
|
+
revision: 0,
|
|
4076
|
+
status: "active",
|
|
4077
|
+
reviews: { plan: [], code: [] },
|
|
4078
|
+
completedTickets: [],
|
|
4079
|
+
finalizeCheckpoint: null,
|
|
4080
|
+
git: { branch: null, mergeBase: null },
|
|
4081
|
+
lease: {
|
|
4082
|
+
workspaceId,
|
|
4083
|
+
lastHeartbeat: now,
|
|
4084
|
+
expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
|
|
4085
|
+
},
|
|
4086
|
+
contextPressure: {
|
|
4087
|
+
level: "low",
|
|
4088
|
+
guideCallCount: 0,
|
|
4089
|
+
ticketsCompleted: 0,
|
|
4090
|
+
compactionCount: 0,
|
|
4091
|
+
eventsLogBytes: 0
|
|
4092
|
+
},
|
|
4093
|
+
pendingProjectMutation: null,
|
|
4094
|
+
resumeFromRevision: null,
|
|
4095
|
+
preCompactState: null,
|
|
4096
|
+
waitingForRetry: false,
|
|
4097
|
+
lastGuideCall: now,
|
|
4098
|
+
startedAt: now,
|
|
4099
|
+
guideCallCount: 0,
|
|
4100
|
+
config: {
|
|
4101
|
+
maxTicketsPerSession: 3,
|
|
4102
|
+
compactThreshold: "high",
|
|
4103
|
+
reviewBackends: ["codex", "agent"]
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
writeSessionSync(dir, state);
|
|
4107
|
+
return state;
|
|
4108
|
+
}
|
|
4109
|
+
function readSession(dir) {
|
|
4110
|
+
const path2 = statePath(dir);
|
|
4111
|
+
let raw;
|
|
4112
|
+
try {
|
|
4113
|
+
raw = readFileSync(path2, "utf-8");
|
|
4114
|
+
} catch {
|
|
4115
|
+
return null;
|
|
4116
|
+
}
|
|
4117
|
+
let parsed;
|
|
4118
|
+
try {
|
|
4119
|
+
parsed = JSON.parse(raw);
|
|
4120
|
+
} catch {
|
|
4121
|
+
return null;
|
|
4122
|
+
}
|
|
4123
|
+
const result = SessionStateSchema.safeParse(parsed);
|
|
4124
|
+
if (!result.success) return null;
|
|
4125
|
+
return result.data;
|
|
4126
|
+
}
|
|
4127
|
+
function writeSessionSync(dir, state) {
|
|
4128
|
+
const path2 = statePath(dir);
|
|
4129
|
+
const updated = { ...state, revision: state.revision + 1 };
|
|
4130
|
+
const content = JSON.stringify(updated, null, 2) + "\n";
|
|
4131
|
+
const tmp = `${path2}.${process.pid}.tmp`;
|
|
4132
|
+
try {
|
|
4133
|
+
writeFileSync(tmp, content, "utf-8");
|
|
4134
|
+
renameSync(tmp, path2);
|
|
4135
|
+
} catch (err) {
|
|
4136
|
+
try {
|
|
4137
|
+
unlinkSync(tmp);
|
|
4138
|
+
} catch {
|
|
4139
|
+
}
|
|
4140
|
+
throw err;
|
|
4141
|
+
}
|
|
4142
|
+
return updated;
|
|
4143
|
+
}
|
|
4144
|
+
function appendEvent(dir, event) {
|
|
4145
|
+
try {
|
|
4146
|
+
const path2 = eventsPath(dir);
|
|
4147
|
+
const line = JSON.stringify(event) + "\n";
|
|
4148
|
+
writeFileSync(path2, line, { flag: "a", encoding: "utf-8" });
|
|
4149
|
+
} catch {
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function deleteSession(root, sessionId) {
|
|
4153
|
+
const dir = sessionDir(root, sessionId);
|
|
4154
|
+
try {
|
|
4155
|
+
rmSync(dir, { recursive: true, force: true });
|
|
4156
|
+
} catch {
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
function refreshLease(state) {
|
|
4160
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4161
|
+
const newCallCount = state.guideCallCount + 1;
|
|
4162
|
+
return {
|
|
4163
|
+
...state,
|
|
4164
|
+
lease: {
|
|
4165
|
+
...state.lease,
|
|
4166
|
+
lastHeartbeat: now,
|
|
4167
|
+
expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
|
|
4168
|
+
},
|
|
4169
|
+
lastGuideCall: now,
|
|
4170
|
+
guideCallCount: newCallCount,
|
|
4171
|
+
contextPressure: {
|
|
4172
|
+
...state.contextPressure,
|
|
4173
|
+
guideCallCount: newCallCount,
|
|
4174
|
+
ticketsCompleted: state.completedTickets?.length ?? 0
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
function isLeaseExpired(state) {
|
|
4179
|
+
if (!state.lease?.expiresAt) return true;
|
|
4180
|
+
const expires = new Date(state.lease.expiresAt).getTime();
|
|
4181
|
+
return Number.isNaN(expires) || expires <= Date.now();
|
|
4182
|
+
}
|
|
4183
|
+
function findActiveSessionFull(root) {
|
|
4184
|
+
const sessDir = sessionsRoot(root);
|
|
4185
|
+
let entries;
|
|
4186
|
+
try {
|
|
4187
|
+
entries = readdirSync(sessDir, { withFileTypes: true });
|
|
4188
|
+
} catch {
|
|
4189
|
+
return null;
|
|
4190
|
+
}
|
|
4191
|
+
let workspaceId;
|
|
4192
|
+
try {
|
|
4193
|
+
workspaceId = deriveWorkspaceId(root);
|
|
4194
|
+
} catch {
|
|
4195
|
+
return null;
|
|
4196
|
+
}
|
|
4197
|
+
let best = null;
|
|
4198
|
+
let bestGuideCall = 0;
|
|
4199
|
+
for (const entry of entries) {
|
|
4200
|
+
if (!entry.isDirectory()) continue;
|
|
4201
|
+
const dir = join6(sessDir, entry.name);
|
|
4202
|
+
const session = readSession(dir);
|
|
4203
|
+
if (!session) continue;
|
|
4204
|
+
if (session.status !== "active") continue;
|
|
4205
|
+
if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
|
|
4206
|
+
if (isLeaseExpired(session)) continue;
|
|
4207
|
+
const guideCall = session.lastGuideCall ? new Date(session.lastGuideCall).getTime() : 0;
|
|
4208
|
+
const guideCallValid = Number.isNaN(guideCall) ? 0 : guideCall;
|
|
4209
|
+
if (!best || guideCallValid > bestGuideCall || guideCallValid === bestGuideCall && session.sessionId > best.state.sessionId) {
|
|
4210
|
+
best = { state: session, dir };
|
|
4211
|
+
bestGuideCall = guideCallValid;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
return best;
|
|
4215
|
+
}
|
|
4216
|
+
function findStaleSessions(root) {
|
|
4217
|
+
const sessDir = sessionsRoot(root);
|
|
4218
|
+
let entries;
|
|
4219
|
+
try {
|
|
4220
|
+
entries = readdirSync(sessDir, { withFileTypes: true });
|
|
4221
|
+
} catch {
|
|
4222
|
+
return [];
|
|
4223
|
+
}
|
|
4224
|
+
let workspaceId;
|
|
4225
|
+
try {
|
|
4226
|
+
workspaceId = deriveWorkspaceId(root);
|
|
4227
|
+
} catch {
|
|
4228
|
+
return [];
|
|
4229
|
+
}
|
|
4230
|
+
const results = [];
|
|
4231
|
+
for (const entry of entries) {
|
|
4232
|
+
if (!entry.isDirectory()) continue;
|
|
4233
|
+
const dir = join6(sessDir, entry.name);
|
|
4234
|
+
const session = readSession(dir);
|
|
4235
|
+
if (!session) continue;
|
|
4236
|
+
if (session.status !== "active") continue;
|
|
4237
|
+
if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
|
|
4238
|
+
if (isLeaseExpired(session)) {
|
|
4239
|
+
results.push({ state: session, dir });
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
return results;
|
|
4243
|
+
}
|
|
4244
|
+
function findSessionById(root, sessionId) {
|
|
4245
|
+
const dir = sessionDir(root, sessionId);
|
|
4246
|
+
if (!existsSync6(dir)) return null;
|
|
4247
|
+
const state = readSession(dir);
|
|
4248
|
+
if (!state) return null;
|
|
4249
|
+
return { state, dir };
|
|
4250
|
+
}
|
|
4251
|
+
async function withSessionLock(root, fn) {
|
|
4252
|
+
const sessDir = sessionsRoot(root);
|
|
4253
|
+
mkdirSync(sessDir, { recursive: true });
|
|
4254
|
+
let release;
|
|
4255
|
+
try {
|
|
4256
|
+
release = await lockfile2.lock(sessDir, {
|
|
4257
|
+
retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
|
|
4258
|
+
stale: 3e4,
|
|
4259
|
+
lockfilePath: join6(sessDir, ".lock")
|
|
4260
|
+
});
|
|
4261
|
+
return await fn();
|
|
4262
|
+
} finally {
|
|
4263
|
+
if (release) {
|
|
4264
|
+
try {
|
|
4265
|
+
await release();
|
|
4266
|
+
} catch {
|
|
3669
4267
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
// src/autonomous/state-machine.ts
|
|
4273
|
+
init_esm_shims();
|
|
4274
|
+
|
|
4275
|
+
// src/autonomous/context-pressure.ts
|
|
4276
|
+
init_esm_shims();
|
|
4277
|
+
function evaluatePressure(state) {
|
|
4278
|
+
const calls = state.contextPressure?.guideCallCount ?? state.guideCallCount ?? 0;
|
|
4279
|
+
const tickets = state.contextPressure?.ticketsCompleted ?? state.completedTickets?.length ?? 0;
|
|
4280
|
+
const eventsBytes = state.contextPressure?.eventsLogBytes ?? 0;
|
|
4281
|
+
if (calls > 45 || tickets >= 3 || eventsBytes > 5e5) return "critical";
|
|
4282
|
+
if (calls >= 30 || tickets >= 2 || eventsBytes > 2e5) return "high";
|
|
4283
|
+
if (calls >= 15 || tickets >= 1 || eventsBytes > 5e4) return "medium";
|
|
4284
|
+
return "low";
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// src/autonomous/review-depth.ts
|
|
4288
|
+
init_esm_shims();
|
|
4289
|
+
var SENSITIVE_PATTERNS = [
|
|
4290
|
+
/\bauth\b/i,
|
|
4291
|
+
/\bsecurity\b/i,
|
|
4292
|
+
/\bmigration/i,
|
|
4293
|
+
/\bconfig\b/i,
|
|
4294
|
+
/\bmiddleware\b/i,
|
|
4295
|
+
/\.env/i
|
|
4296
|
+
];
|
|
4297
|
+
function assessRisk(diffStats, changedFiles) {
|
|
4298
|
+
let level = "low";
|
|
4299
|
+
if (diffStats) {
|
|
4300
|
+
const total = diffStats.totalLines;
|
|
4301
|
+
if (total > 200) level = "high";
|
|
4302
|
+
else if (total >= 50) level = "medium";
|
|
4303
|
+
}
|
|
4304
|
+
if (changedFiles && level !== "high") {
|
|
4305
|
+
const hasSensitive = changedFiles.some(
|
|
4306
|
+
(f) => SENSITIVE_PATTERNS.some((p) => p.test(f))
|
|
4307
|
+
);
|
|
4308
|
+
if (hasSensitive) {
|
|
4309
|
+
level = level === "low" ? "medium" : "high";
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
return level;
|
|
4313
|
+
}
|
|
4314
|
+
function requiredRounds(risk) {
|
|
4315
|
+
switch (risk) {
|
|
4316
|
+
case "low":
|
|
4317
|
+
return 1;
|
|
4318
|
+
case "medium":
|
|
4319
|
+
return 2;
|
|
4320
|
+
case "high":
|
|
4321
|
+
return 3;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
function nextReviewer(previousRounds, backends) {
|
|
4325
|
+
if (backends.length === 0) return "agent";
|
|
4326
|
+
if (backends.length === 1) return backends[0];
|
|
4327
|
+
if (previousRounds.length === 0) return backends[0];
|
|
4328
|
+
const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
|
|
4329
|
+
const lastIndex = backends.indexOf(lastReviewer);
|
|
4330
|
+
if (lastIndex === -1) return backends[0];
|
|
4331
|
+
return backends[(lastIndex + 1) % backends.length];
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
// src/autonomous/git-inspector.ts
|
|
4335
|
+
init_esm_shims();
|
|
4336
|
+
import { execFile } from "child_process";
|
|
4337
|
+
var GIT_TIMEOUT = 1e4;
|
|
4338
|
+
async function git(cwd, args, parse) {
|
|
4339
|
+
return new Promise((resolve9) => {
|
|
4340
|
+
execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
4341
|
+
if (err) {
|
|
4342
|
+
const message = stderr?.trim() || err.message || "unknown git error";
|
|
4343
|
+
resolve9({ ok: false, reason: "git_error", message });
|
|
4344
|
+
return;
|
|
3679
4345
|
}
|
|
3680
4346
|
try {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (found.status !== "archived") throw new Error(`Expected archived, got ${found.status}`);
|
|
3685
|
-
record("note", "verify update", true, `Verified ${noteId} status = archived`);
|
|
3686
|
-
} catch (err) {
|
|
3687
|
-
record("note", "verify update", false, errMsg(err));
|
|
4347
|
+
resolve9({ ok: true, data: parse(stdout) });
|
|
4348
|
+
} catch (parseErr) {
|
|
4349
|
+
resolve9({ ok: false, reason: "parse_error", message: parseErr.message });
|
|
3688
4350
|
}
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
4351
|
+
});
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
async function gitStatus(cwd) {
|
|
4355
|
+
return git(
|
|
4356
|
+
cwd,
|
|
4357
|
+
["status", "--porcelain"],
|
|
4358
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
async function gitHead(cwd) {
|
|
4362
|
+
const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
|
|
4363
|
+
if (!hashResult.ok) return hashResult;
|
|
4364
|
+
const branchResult = await gitBranch(cwd);
|
|
4365
|
+
return {
|
|
4366
|
+
ok: true,
|
|
4367
|
+
data: {
|
|
4368
|
+
hash: hashResult.data,
|
|
4369
|
+
branch: branchResult.ok ? branchResult.data : null
|
|
4370
|
+
}
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
async function gitBranch(cwd) {
|
|
4374
|
+
return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
|
|
4375
|
+
}
|
|
4376
|
+
async function gitMergeBase(cwd, base) {
|
|
4377
|
+
return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
|
|
4378
|
+
}
|
|
4379
|
+
async function gitDiffStat(cwd, base) {
|
|
4380
|
+
return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
|
|
4381
|
+
}
|
|
4382
|
+
async function gitDiffNames(cwd, base) {
|
|
4383
|
+
return git(
|
|
4384
|
+
cwd,
|
|
4385
|
+
["diff", "--name-only", base],
|
|
4386
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
async function gitBlobHash(cwd, file) {
|
|
4390
|
+
return git(cwd, ["hash-object", file], (out) => out.trim());
|
|
4391
|
+
}
|
|
4392
|
+
async function gitDiffCachedNames(cwd) {
|
|
4393
|
+
return git(
|
|
4394
|
+
cwd,
|
|
4395
|
+
["diff", "--cached", "--name-only"],
|
|
4396
|
+
(out) => out.split("\n").filter((l) => l.length > 0)
|
|
4397
|
+
);
|
|
4398
|
+
}
|
|
4399
|
+
function parseDiffNumstat(out) {
|
|
4400
|
+
const lines = out.split("\n").filter((l) => l.length > 0);
|
|
4401
|
+
let insertions = 0;
|
|
4402
|
+
let deletions = 0;
|
|
4403
|
+
let filesChanged = 0;
|
|
4404
|
+
for (const line of lines) {
|
|
4405
|
+
const parts = line.split(" ");
|
|
4406
|
+
if (parts.length < 3) continue;
|
|
4407
|
+
const added = parseInt(parts[0], 10);
|
|
4408
|
+
const removed = parseInt(parts[1], 10);
|
|
4409
|
+
if (!Number.isNaN(added)) insertions += added;
|
|
4410
|
+
if (!Number.isNaN(removed)) deletions += removed;
|
|
4411
|
+
filesChanged++;
|
|
4412
|
+
}
|
|
4413
|
+
return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// src/autonomous/guide.ts
|
|
4417
|
+
init_project_loader();
|
|
4418
|
+
init_snapshot();
|
|
4419
|
+
init_snapshot();
|
|
4420
|
+
init_queries();
|
|
4421
|
+
var workspaceLocks = /* @__PURE__ */ new Map();
|
|
4422
|
+
async function handleAutonomousGuide(root, args) {
|
|
4423
|
+
const wsId = deriveWorkspaceId(root);
|
|
4424
|
+
const prev = workspaceLocks.get(wsId) ?? Promise.resolve();
|
|
4425
|
+
const current = prev.then(async () => {
|
|
4426
|
+
return withSessionLock(root, () => handleGuideInner(root, args));
|
|
4427
|
+
});
|
|
4428
|
+
workspaceLocks.set(wsId, current.then(() => {
|
|
4429
|
+
}, () => {
|
|
4430
|
+
}));
|
|
4431
|
+
try {
|
|
4432
|
+
return await current;
|
|
4433
|
+
} catch (err) {
|
|
4434
|
+
return guideError(err);
|
|
4435
|
+
} finally {
|
|
4436
|
+
const stored = workspaceLocks.get(wsId);
|
|
4437
|
+
if (stored) {
|
|
4438
|
+
stored.then(() => {
|
|
4439
|
+
if (workspaceLocks.get(wsId) === stored) {
|
|
4440
|
+
workspaceLocks.delete(wsId);
|
|
4441
|
+
}
|
|
4442
|
+
}, () => {
|
|
4443
|
+
if (workspaceLocks.get(wsId) === stored) {
|
|
4444
|
+
workspaceLocks.delete(wsId);
|
|
4445
|
+
}
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
async function handleGuideInner(root, args) {
|
|
4451
|
+
switch (args.action) {
|
|
4452
|
+
case "start":
|
|
4453
|
+
return handleStart(root, args);
|
|
4454
|
+
case "report":
|
|
4455
|
+
return handleReport(root, args);
|
|
4456
|
+
case "resume":
|
|
4457
|
+
return handleResume(root, args);
|
|
4458
|
+
case "pre_compact":
|
|
4459
|
+
return handlePreCompact(root, args);
|
|
4460
|
+
case "cancel":
|
|
4461
|
+
return handleCancel(root, args);
|
|
4462
|
+
default:
|
|
4463
|
+
return guideError(new Error(`Unknown action: ${args.action}`));
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
async function handleStart(root, args) {
|
|
4467
|
+
const existing = findActiveSessionFull(root);
|
|
4468
|
+
if (existing && !isLeaseExpired(existing.state)) {
|
|
4469
|
+
return guideError(new Error(
|
|
4470
|
+
`Active session ${existing.state.sessionId} already exists for this workspace. Use action: "resume" to continue or "cancel" to end it.`
|
|
4471
|
+
));
|
|
4472
|
+
}
|
|
4473
|
+
const staleSessions = findStaleSessions(root);
|
|
4474
|
+
for (const stale of staleSessions) {
|
|
4475
|
+
writeSessionSync(stale.dir, { ...stale.state, status: "superseded" });
|
|
4476
|
+
}
|
|
4477
|
+
const wsId = deriveWorkspaceId(root);
|
|
4478
|
+
const recipe = "coding";
|
|
4479
|
+
const session = createSession(root, recipe, wsId);
|
|
4480
|
+
const dir = sessionDir(root, session.sessionId);
|
|
4481
|
+
try {
|
|
4482
|
+
const headResult = await gitHead(root);
|
|
4483
|
+
if (!headResult.ok) {
|
|
4484
|
+
deleteSession(root, session.sessionId);
|
|
4485
|
+
return guideError(new Error("This directory is not a git repository or git is not available. Autonomous mode requires git."));
|
|
4486
|
+
}
|
|
4487
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
4488
|
+
if (stagedResult.ok && stagedResult.data.length > 0) {
|
|
4489
|
+
deleteSession(root, session.sessionId);
|
|
4490
|
+
return guideError(new Error(
|
|
4491
|
+
`Cannot start: ${stagedResult.data.length} staged file(s). Unstage with \`git restore --staged .\` or commit them first, then call start again.
|
|
4492
|
+
|
|
4493
|
+
Staged: ${stagedResult.data.join(", ")}`
|
|
4494
|
+
));
|
|
4495
|
+
}
|
|
4496
|
+
const statusResult = await gitStatus(root);
|
|
4497
|
+
let mergeBaseResult = await gitMergeBase(root, "main");
|
|
4498
|
+
if (!mergeBaseResult.ok) mergeBaseResult = await gitMergeBase(root, "master");
|
|
4499
|
+
const porcelainLines = statusResult.ok ? statusResult.data : [];
|
|
4500
|
+
const dirtyTracked = {};
|
|
4501
|
+
const untrackedPaths = [];
|
|
4502
|
+
for (const line of porcelainLines) {
|
|
4503
|
+
if (line.startsWith("??")) {
|
|
4504
|
+
untrackedPaths.push(line.slice(3).trim());
|
|
4505
|
+
} else if (line.length > 3) {
|
|
4506
|
+
const filePath = line.slice(3).trim();
|
|
4507
|
+
const hashResult = await gitBlobHash(root, filePath);
|
|
4508
|
+
dirtyTracked[filePath] = { blobHash: hashResult.ok ? hashResult.data : "" };
|
|
3695
4509
|
}
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
4510
|
+
}
|
|
4511
|
+
if (Object.keys(dirtyTracked).length > 0) {
|
|
4512
|
+
deleteSession(root, session.sessionId);
|
|
4513
|
+
const dirtyFiles = Object.keys(dirtyTracked).join(", ");
|
|
4514
|
+
return guideError(new Error(
|
|
4515
|
+
`Cannot start: ${Object.keys(dirtyTracked).length} dirty tracked file(s): ${dirtyFiles}. Create a feature branch or stash changes first, then call start again.`
|
|
4516
|
+
));
|
|
4517
|
+
}
|
|
4518
|
+
let updated = {
|
|
4519
|
+
...session,
|
|
4520
|
+
state: "PICK_TICKET",
|
|
4521
|
+
previousState: "INIT",
|
|
4522
|
+
git: {
|
|
4523
|
+
branch: headResult.data.branch,
|
|
4524
|
+
initHead: headResult.data.hash,
|
|
4525
|
+
mergeBase: mergeBaseResult.ok ? mergeBaseResult.data : null,
|
|
4526
|
+
expectedHead: headResult.data.hash,
|
|
4527
|
+
baseline: {
|
|
4528
|
+
porcelain: porcelainLines,
|
|
4529
|
+
dirtyTrackedFiles: dirtyTracked,
|
|
4530
|
+
untrackedPaths
|
|
4531
|
+
}
|
|
3703
4532
|
}
|
|
4533
|
+
};
|
|
4534
|
+
const { state: projectState, warnings } = await loadProject(root);
|
|
4535
|
+
const handoversDir = join7(root, ".story", "handovers");
|
|
4536
|
+
const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
|
|
4537
|
+
let handoverText = "";
|
|
4538
|
+
try {
|
|
4539
|
+
const handoverResult = await handleHandoverLatest(ctx, 3);
|
|
4540
|
+
handoverText = handoverResult.output;
|
|
4541
|
+
} catch {
|
|
3704
4542
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
}
|
|
3712
|
-
|
|
4543
|
+
let recapText = "";
|
|
4544
|
+
try {
|
|
4545
|
+
const snapshotInfo = await loadLatestSnapshot(root);
|
|
4546
|
+
const recap = buildRecap(projectState, snapshotInfo);
|
|
4547
|
+
if (recap.changes) {
|
|
4548
|
+
recapText = "Changes since last snapshot available.";
|
|
4549
|
+
}
|
|
4550
|
+
} catch {
|
|
4551
|
+
}
|
|
4552
|
+
const rulesText = readFileSafe(join7(root, "RULES.md"));
|
|
4553
|
+
const strategiesText = readFileSafe(join7(root, "WORK_STRATEGIES.md"));
|
|
4554
|
+
const digestParts = [
|
|
4555
|
+
handoverText ? `## Recent Handovers
|
|
4556
|
+
|
|
4557
|
+
${handoverText}` : "",
|
|
4558
|
+
recapText ? `## Recap
|
|
4559
|
+
|
|
4560
|
+
${recapText}` : "",
|
|
4561
|
+
rulesText ? `## Development Rules
|
|
4562
|
+
|
|
4563
|
+
${rulesText}` : "",
|
|
4564
|
+
strategiesText ? `## Work Strategies
|
|
4565
|
+
|
|
4566
|
+
${strategiesText}` : ""
|
|
4567
|
+
].filter(Boolean);
|
|
4568
|
+
const digest = digestParts.join("\n\n---\n\n");
|
|
4569
|
+
try {
|
|
4570
|
+
writeFileSync2(join7(dir, "context-digest.md"), digest, "utf-8");
|
|
4571
|
+
} catch {
|
|
4572
|
+
}
|
|
4573
|
+
const nextResult = nextTickets(projectState, 5);
|
|
4574
|
+
let candidatesText = "";
|
|
4575
|
+
if (nextResult.kind === "found") {
|
|
4576
|
+
candidatesText = nextResult.candidates.map(
|
|
4577
|
+
(c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type}, phase: ${c.ticket.phase ?? "unphased"})${c.unblockImpact.wouldUnblock.length > 0 ? ` \u2014 unblocks ${c.unblockImpact.wouldUnblock.map((t) => t.id).join(", ")}` : ""}`
|
|
4578
|
+
).join("\n");
|
|
4579
|
+
} else if (nextResult.kind === "all_complete") {
|
|
4580
|
+
candidatesText = "All tickets are complete. No work to do.";
|
|
4581
|
+
} else if (nextResult.kind === "all_blocked") {
|
|
4582
|
+
candidatesText = "All remaining tickets are blocked.";
|
|
4583
|
+
} else {
|
|
4584
|
+
candidatesText = "No tickets found.";
|
|
4585
|
+
}
|
|
4586
|
+
const recResult = recommend(projectState, 5);
|
|
4587
|
+
let recsText = "";
|
|
4588
|
+
if (recResult.recommendations.length > 0) {
|
|
4589
|
+
const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
|
|
4590
|
+
if (ticketRecs.length > 0) {
|
|
4591
|
+
recsText = "\n\n**Recommended:**\n" + ticketRecs.map(
|
|
4592
|
+
(r) => `- ${r.id}: ${r.title} (${r.reason})`
|
|
4593
|
+
).join("\n");
|
|
3713
4594
|
}
|
|
3714
4595
|
}
|
|
4596
|
+
updated = refreshLease(updated);
|
|
4597
|
+
const pressure = evaluatePressure(updated);
|
|
4598
|
+
updated = { ...updated, contextPressure: { ...updated.contextPressure, level: pressure } };
|
|
4599
|
+
const written = writeSessionSync(dir, updated);
|
|
4600
|
+
appendEvent(dir, {
|
|
4601
|
+
rev: written.revision,
|
|
4602
|
+
type: "start",
|
|
4603
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4604
|
+
data: { recipe, branch: written.git.branch, head: written.git.initHead }
|
|
4605
|
+
});
|
|
4606
|
+
const topCandidate = nextResult.kind === "found" ? nextResult.candidates[0] : null;
|
|
4607
|
+
const instruction = [
|
|
4608
|
+
"# Autonomous Session Started",
|
|
4609
|
+
"",
|
|
4610
|
+
"You are now in autonomous mode. Work continuously until all tickets are done or the session limit is reached.",
|
|
4611
|
+
"Do NOT stop to summarize. Do NOT ask the user. Pick a ticket and start working immediately.",
|
|
4612
|
+
"",
|
|
4613
|
+
"## Ticket Candidates",
|
|
4614
|
+
"",
|
|
4615
|
+
candidatesText,
|
|
4616
|
+
recsText,
|
|
4617
|
+
"",
|
|
4618
|
+
topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
|
|
4619
|
+
"```json",
|
|
4620
|
+
topCandidate ? `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
|
|
4621
|
+
"```"
|
|
4622
|
+
].join("\n");
|
|
4623
|
+
return guideResult(updated, "PICK_TICKET", {
|
|
4624
|
+
instruction,
|
|
4625
|
+
reminders: [
|
|
4626
|
+
"Do NOT use Claude Code's plan mode \u2014 write plans as markdown files.",
|
|
4627
|
+
"Do NOT ask the user for confirmation or approval.",
|
|
4628
|
+
"Do NOT stop or summarize between tickets \u2014 call autonomous_guide IMMEDIATELY.",
|
|
4629
|
+
"You are in autonomous mode \u2014 continue working until done."
|
|
4630
|
+
],
|
|
4631
|
+
transitionedFrom: "INIT"
|
|
4632
|
+
});
|
|
4633
|
+
} catch (err) {
|
|
4634
|
+
deleteSession(root, session.sessionId);
|
|
4635
|
+
throw err;
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
async function handleReport(root, args) {
|
|
4639
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for report action"));
|
|
4640
|
+
if (!args.report) return guideError(new Error("report field is required for report action"));
|
|
4641
|
+
const info = findSessionById(root, args.sessionId);
|
|
4642
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
4643
|
+
let state = refreshLease(info.state);
|
|
4644
|
+
const currentState = state.state;
|
|
4645
|
+
const report = args.report;
|
|
4646
|
+
switch (currentState) {
|
|
4647
|
+
case "PICK_TICKET":
|
|
4648
|
+
return handleReportPickTicket(root, info.dir, state, report);
|
|
4649
|
+
case "PLAN":
|
|
4650
|
+
return handleReportPlan(root, info.dir, state, report);
|
|
4651
|
+
case "PLAN_REVIEW":
|
|
4652
|
+
return handleReportPlanReview(root, info.dir, state, report);
|
|
4653
|
+
case "IMPLEMENT":
|
|
4654
|
+
return handleReportImplement(root, info.dir, state, report);
|
|
4655
|
+
case "CODE_REVIEW":
|
|
4656
|
+
return handleReportCodeReview(root, info.dir, state, report);
|
|
4657
|
+
case "FINALIZE":
|
|
4658
|
+
return handleReportFinalize(root, info.dir, state, report);
|
|
4659
|
+
case "COMPLETE":
|
|
4660
|
+
return handleReportComplete(root, info.dir, state, report);
|
|
4661
|
+
case "HANDOVER":
|
|
4662
|
+
return handleReportHandover(root, info.dir, state, report);
|
|
4663
|
+
default:
|
|
4664
|
+
return guideError(new Error(`Cannot report at state ${currentState}`));
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
async function handleReportPickTicket(root, dir, state, report) {
|
|
4668
|
+
const ticketId = report.ticketId;
|
|
4669
|
+
if (!ticketId) return guideError(new Error("report.ticketId is required when picking a ticket"));
|
|
4670
|
+
const { state: projectState } = await loadProject(root);
|
|
4671
|
+
const ticket = projectState.ticketByID(ticketId);
|
|
4672
|
+
if (!ticket) return guideError(new Error(`Ticket ${ticketId} not found`));
|
|
4673
|
+
if (projectState.isBlocked(ticket)) return guideError(new Error(`Ticket ${ticketId} is blocked`));
|
|
4674
|
+
const written = writeSessionSync(dir, {
|
|
4675
|
+
...state,
|
|
4676
|
+
state: "PLAN",
|
|
4677
|
+
previousState: "PICK_TICKET",
|
|
4678
|
+
ticket: { id: ticket.id, title: ticket.title, claimed: true }
|
|
4679
|
+
});
|
|
4680
|
+
appendEvent(dir, {
|
|
4681
|
+
rev: written.revision,
|
|
4682
|
+
type: "ticket_picked",
|
|
4683
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4684
|
+
data: { ticketId: ticket.id, title: ticket.title }
|
|
4685
|
+
});
|
|
4686
|
+
return guideResult(written, "PLAN", {
|
|
4687
|
+
instruction: [
|
|
4688
|
+
`# Plan for ${ticket.id}: ${ticket.title}`,
|
|
4689
|
+
"",
|
|
4690
|
+
ticket.description ? `## Ticket Description
|
|
4691
|
+
|
|
4692
|
+
${ticket.description}` : "",
|
|
4693
|
+
"",
|
|
4694
|
+
`Write an implementation plan for this ticket. Save it to \`.story/sessions/${state.sessionId}/plan.md\`.`,
|
|
4695
|
+
"",
|
|
4696
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
4697
|
+
"```json",
|
|
4698
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_written" } }`,
|
|
4699
|
+
"```"
|
|
4700
|
+
].join("\n"),
|
|
4701
|
+
reminders: [
|
|
4702
|
+
"Write the plan as a markdown file \u2014 do NOT use Claude Code's plan mode.",
|
|
4703
|
+
"Do NOT ask the user for approval."
|
|
4704
|
+
],
|
|
4705
|
+
transitionedFrom: "PICK_TICKET"
|
|
4706
|
+
});
|
|
4707
|
+
}
|
|
4708
|
+
async function handleReportPlan(root, dir, state, report) {
|
|
4709
|
+
const planPath = join7(dir, "plan.md");
|
|
4710
|
+
if (!existsSync7(planPath)) {
|
|
4711
|
+
return guideResult(state, "PLAN", {
|
|
4712
|
+
instruction: `Plan file not found at ${planPath}. Write your plan there and call me again.`,
|
|
4713
|
+
reminders: ["Save plan to .story/sessions/<id>/plan.md"]
|
|
4714
|
+
});
|
|
3715
4715
|
}
|
|
3716
|
-
const
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
4716
|
+
const planContent = readFileSafe(planPath);
|
|
4717
|
+
if (!planContent || planContent.trim().length === 0) {
|
|
4718
|
+
return guideResult(state, "PLAN", {
|
|
4719
|
+
instruction: "Plan file is empty. Write your implementation plan and call me again.",
|
|
4720
|
+
reminders: []
|
|
4721
|
+
});
|
|
4722
|
+
}
|
|
4723
|
+
const risk = assessRisk(void 0, void 0);
|
|
4724
|
+
const written = writeSessionSync(dir, {
|
|
4725
|
+
...state,
|
|
4726
|
+
state: "PLAN_REVIEW",
|
|
4727
|
+
previousState: "PLAN",
|
|
4728
|
+
ticket: state.ticket ? { ...state.ticket, risk } : state.ticket
|
|
4729
|
+
});
|
|
4730
|
+
appendEvent(dir, {
|
|
4731
|
+
rev: written.revision,
|
|
4732
|
+
type: "plan_written",
|
|
4733
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4734
|
+
data: { planLength: planContent.length, risk }
|
|
4735
|
+
});
|
|
4736
|
+
const backends = state.config.reviewBackends;
|
|
4737
|
+
const reviewer = nextReviewer([], backends);
|
|
4738
|
+
const rounds = requiredRounds(risk);
|
|
4739
|
+
return guideResult(written, "PLAN_REVIEW", {
|
|
4740
|
+
instruction: [
|
|
4741
|
+
`# Plan Review \u2014 Round 1 of ${rounds} minimum`,
|
|
4742
|
+
"",
|
|
4743
|
+
`Run a plan review using **${reviewer}**.`,
|
|
4744
|
+
"",
|
|
4745
|
+
reviewer === "codex" ? `Call \`review_plan\` MCP tool with the plan content.` : `Launch a code review agent to review the plan.`,
|
|
4746
|
+
"",
|
|
4747
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
4748
|
+
"```json",
|
|
4749
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|reject>", "findings": [...] } }`,
|
|
4750
|
+
"```"
|
|
4751
|
+
].join("\n"),
|
|
4752
|
+
reminders: ["Report the exact verdict and findings from the reviewer."],
|
|
4753
|
+
transitionedFrom: "PLAN"
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
async function handleReportPlanReview(root, dir, state, report) {
|
|
4757
|
+
const verdict = report.verdict;
|
|
4758
|
+
if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
|
|
4759
|
+
return guideResult(state, "PLAN_REVIEW", {
|
|
4760
|
+
instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
|
|
4761
|
+
reminders: []
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
const planReviews = [...state.reviews.plan];
|
|
4765
|
+
const roundNum = planReviews.length + 1;
|
|
4766
|
+
const findings = report.findings ?? [];
|
|
4767
|
+
const backends = state.config.reviewBackends;
|
|
4768
|
+
const reviewerBackend = nextReviewer(planReviews, backends);
|
|
4769
|
+
planReviews.push({
|
|
4770
|
+
round: roundNum,
|
|
4771
|
+
reviewer: reviewerBackend,
|
|
4772
|
+
verdict,
|
|
4773
|
+
findingCount: findings.length,
|
|
4774
|
+
criticalCount: findings.filter((f) => f.severity === "critical").length,
|
|
4775
|
+
majorCount: findings.filter((f) => f.severity === "major").length,
|
|
4776
|
+
suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
|
|
4777
|
+
codexSessionId: report.reviewerSessionId,
|
|
4778
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4779
|
+
});
|
|
4780
|
+
const risk = state.ticket?.risk ?? "low";
|
|
4781
|
+
const minRounds = requiredRounds(risk);
|
|
4782
|
+
const hasCriticalOrMajor = findings.some(
|
|
4783
|
+
(f) => f.severity === "critical" || f.severity === "major"
|
|
4784
|
+
);
|
|
4785
|
+
let nextState;
|
|
4786
|
+
if (verdict === "reject") {
|
|
4787
|
+
nextState = "PLAN";
|
|
4788
|
+
} else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
|
|
4789
|
+
nextState = "IMPLEMENT";
|
|
4790
|
+
} else if (roundNum >= 5) {
|
|
4791
|
+
nextState = "IMPLEMENT";
|
|
4792
|
+
} else {
|
|
4793
|
+
nextState = "PLAN_REVIEW";
|
|
4794
|
+
}
|
|
4795
|
+
const written = writeSessionSync(dir, {
|
|
4796
|
+
...state,
|
|
4797
|
+
state: nextState,
|
|
4798
|
+
previousState: "PLAN_REVIEW",
|
|
4799
|
+
reviews: { ...state.reviews, plan: planReviews }
|
|
4800
|
+
});
|
|
4801
|
+
appendEvent(dir, {
|
|
4802
|
+
rev: written.revision,
|
|
4803
|
+
type: "plan_review",
|
|
4804
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4805
|
+
data: { round: roundNum, verdict, findingCount: findings.length }
|
|
4806
|
+
});
|
|
4807
|
+
if (nextState === "PLAN") {
|
|
4808
|
+
return guideResult(written, "PLAN", {
|
|
4809
|
+
instruction: 'Plan was rejected. Revise and rewrite the plan, then call me with completedAction: "plan_written".',
|
|
4810
|
+
reminders: [],
|
|
4811
|
+
transitionedFrom: "PLAN_REVIEW"
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
if (nextState === "IMPLEMENT") {
|
|
4815
|
+
return guideResult(written, "IMPLEMENT", {
|
|
4816
|
+
instruction: [
|
|
4817
|
+
"# Implement",
|
|
4818
|
+
"",
|
|
4819
|
+
"Plan review passed. Implement the plan now.",
|
|
4820
|
+
"",
|
|
4821
|
+
"When done, call `claudestory_autonomous_guide` with:",
|
|
4822
|
+
"```json",
|
|
4823
|
+
`{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "implementation_done" } }`,
|
|
4824
|
+
"```"
|
|
4825
|
+
].join("\n"),
|
|
4826
|
+
reminders: ["Call autonomous_guide when implementation is complete."],
|
|
4827
|
+
transitionedFrom: "PLAN_REVIEW"
|
|
4828
|
+
});
|
|
4829
|
+
}
|
|
4830
|
+
const reviewer = nextReviewer(planReviews, backends);
|
|
4831
|
+
return guideResult(written, "PLAN_REVIEW", {
|
|
4832
|
+
instruction: [
|
|
4833
|
+
`# Plan Review \u2014 Round ${roundNum + 1}`,
|
|
4834
|
+
"",
|
|
4835
|
+
hasCriticalOrMajor ? `Round ${roundNum} found ${findings.filter((f) => f.severity === "critical" || f.severity === "major").length} critical/major finding(s). Address them, then re-review with **${reviewer}**.` : `Round ${roundNum} complete. Run round ${roundNum + 1} with **${reviewer}**.`,
|
|
4836
|
+
"",
|
|
4837
|
+
"Report verdict and findings as before."
|
|
4838
|
+
].join("\n"),
|
|
4839
|
+
reminders: ["Address findings before re-reviewing."]
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
async function handleReportImplement(root, dir, state, report) {
|
|
4843
|
+
let realizedRisk = state.ticket?.risk ?? "low";
|
|
4844
|
+
const mergeBase = state.git.mergeBase;
|
|
4845
|
+
if (mergeBase) {
|
|
4846
|
+
const diffResult = await gitDiffStat(root, mergeBase);
|
|
4847
|
+
const namesResult = await gitDiffNames(root, mergeBase);
|
|
4848
|
+
if (diffResult.ok) {
|
|
4849
|
+
realizedRisk = assessRisk(diffResult.data, namesResult.ok ? namesResult.data : void 0);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
const backends = state.config.reviewBackends;
|
|
4853
|
+
const codeReviews = state.reviews.code;
|
|
4854
|
+
const reviewer = nextReviewer(codeReviews, backends);
|
|
4855
|
+
const rounds = requiredRounds(realizedRisk);
|
|
4856
|
+
const written = writeSessionSync(dir, {
|
|
4857
|
+
...state,
|
|
4858
|
+
state: "CODE_REVIEW",
|
|
4859
|
+
previousState: "IMPLEMENT",
|
|
4860
|
+
ticket: state.ticket ? { ...state.ticket, realizedRisk } : state.ticket
|
|
4861
|
+
});
|
|
4862
|
+
appendEvent(dir, {
|
|
4863
|
+
rev: written.revision,
|
|
4864
|
+
type: "implementation_done",
|
|
4865
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4866
|
+
data: { realizedRisk }
|
|
4867
|
+
});
|
|
4868
|
+
return guideResult(written, "CODE_REVIEW", {
|
|
4869
|
+
instruction: [
|
|
4870
|
+
`# Code Review \u2014 Round 1 of ${rounds} minimum`,
|
|
4871
|
+
"",
|
|
4872
|
+
`Realized risk: **${realizedRisk}**${realizedRisk !== state.ticket?.risk ? ` (was ${state.ticket?.risk})` : ""}.`,
|
|
4873
|
+
"",
|
|
4874
|
+
`Run a code review using **${reviewer}**. Capture the git diff and pass it to the reviewer.`,
|
|
4875
|
+
"",
|
|
4876
|
+
"When done, report verdict and findings."
|
|
4877
|
+
].join("\n"),
|
|
4878
|
+
reminders: ["Capture diff with `git diff` and pass to reviewer."],
|
|
4879
|
+
transitionedFrom: "IMPLEMENT"
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
async function handleReportCodeReview(root, dir, state, report) {
|
|
4883
|
+
const verdict = report.verdict;
|
|
4884
|
+
if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
|
|
4885
|
+
return guideResult(state, "CODE_REVIEW", {
|
|
4886
|
+
instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
|
|
4887
|
+
reminders: []
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
const codeReviews = [...state.reviews.code];
|
|
4891
|
+
const roundNum = codeReviews.length + 1;
|
|
4892
|
+
const findings = report.findings ?? [];
|
|
4893
|
+
const backends = state.config.reviewBackends;
|
|
4894
|
+
const reviewerBackend = nextReviewer(codeReviews, backends);
|
|
4895
|
+
codeReviews.push({
|
|
4896
|
+
round: roundNum,
|
|
4897
|
+
reviewer: reviewerBackend,
|
|
4898
|
+
verdict,
|
|
4899
|
+
findingCount: findings.length,
|
|
4900
|
+
criticalCount: findings.filter((f) => f.severity === "critical").length,
|
|
4901
|
+
majorCount: findings.filter((f) => f.severity === "major").length,
|
|
4902
|
+
suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
|
|
4903
|
+
codexSessionId: report.reviewerSessionId,
|
|
4904
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4905
|
+
});
|
|
4906
|
+
const risk = state.ticket?.realizedRisk ?? state.ticket?.risk ?? "low";
|
|
4907
|
+
const minRounds = requiredRounds(risk);
|
|
4908
|
+
const hasCriticalOrMajor = findings.some(
|
|
4909
|
+
(f) => f.severity === "critical" || f.severity === "major"
|
|
4910
|
+
);
|
|
4911
|
+
const planRedirect = findings.some((f) => f.recommendedNextState === "PLAN");
|
|
4912
|
+
let nextState;
|
|
4913
|
+
if (verdict === "reject" && planRedirect) {
|
|
4914
|
+
nextState = "PLAN";
|
|
4915
|
+
} else if (verdict === "reject") {
|
|
4916
|
+
nextState = "IMPLEMENT";
|
|
4917
|
+
} else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
|
|
4918
|
+
nextState = "FINALIZE";
|
|
4919
|
+
} else if (roundNum >= 5) {
|
|
4920
|
+
nextState = "FINALIZE";
|
|
4921
|
+
} else {
|
|
4922
|
+
nextState = "CODE_REVIEW";
|
|
4923
|
+
}
|
|
4924
|
+
const written = writeSessionSync(dir, {
|
|
4925
|
+
...state,
|
|
4926
|
+
state: nextState,
|
|
4927
|
+
previousState: "CODE_REVIEW",
|
|
4928
|
+
reviews: { ...state.reviews, code: codeReviews }
|
|
4929
|
+
});
|
|
4930
|
+
appendEvent(dir, {
|
|
4931
|
+
rev: written.revision,
|
|
4932
|
+
type: "code_review",
|
|
4933
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4934
|
+
data: { round: roundNum, verdict, findingCount: findings.length }
|
|
4935
|
+
});
|
|
4936
|
+
if (nextState === "PLAN") {
|
|
4937
|
+
return guideResult(written, "PLAN", {
|
|
4938
|
+
instruction: 'Code review recommends rethinking the approach. Revise the plan and call me with completedAction: "plan_written".',
|
|
4939
|
+
reminders: [],
|
|
4940
|
+
transitionedFrom: "CODE_REVIEW"
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
if (nextState === "IMPLEMENT") {
|
|
4944
|
+
return guideResult(written, "IMPLEMENT", {
|
|
4945
|
+
instruction: 'Code review requested changes. Fix the issues and call me with completedAction: "implementation_done".',
|
|
4946
|
+
reminders: ["Address all critical/major findings before re-submitting."],
|
|
4947
|
+
transitionedFrom: "CODE_REVIEW"
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
if (nextState === "FINALIZE") {
|
|
4951
|
+
return guideResult(written, "FINALIZE", {
|
|
4952
|
+
instruction: [
|
|
4953
|
+
"# Finalize",
|
|
4954
|
+
"",
|
|
4955
|
+
"Code review passed. Time to commit.",
|
|
4956
|
+
"",
|
|
4957
|
+
state.ticket ? `1. Update ticket ${state.ticket.id} status to "complete" in .story/` : "",
|
|
4958
|
+
"2. Stage all changed files (code + .story/ changes)",
|
|
4959
|
+
'3. Call me with completedAction: "files_staged"'
|
|
4960
|
+
].filter(Boolean).join("\n"),
|
|
4961
|
+
reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
|
|
4962
|
+
transitionedFrom: "CODE_REVIEW"
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
const reviewer = nextReviewer(codeReviews, backends);
|
|
4966
|
+
return guideResult(written, "CODE_REVIEW", {
|
|
4967
|
+
instruction: `Code review round ${roundNum} found issues. Fix them and re-review with **${reviewer}**.`,
|
|
4968
|
+
reminders: []
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
async function handleReportFinalize(root, dir, state, report) {
|
|
4972
|
+
const action = report.completedAction;
|
|
4973
|
+
const checkpoint = state.finalizeCheckpoint;
|
|
4974
|
+
if (action === "files_staged" && (!checkpoint || checkpoint === "staged")) {
|
|
4975
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
4976
|
+
if (!stagedResult.ok || stagedResult.data.length === 0) {
|
|
4977
|
+
return guideResult(state, "FINALIZE", {
|
|
4978
|
+
instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".',
|
|
4979
|
+
reminders: []
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4982
|
+
const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "staged" });
|
|
4983
|
+
return guideResult(written, "FINALIZE", {
|
|
4984
|
+
instruction: [
|
|
4985
|
+
"Files staged. Now run pre-commit checks.",
|
|
4986
|
+
"",
|
|
4987
|
+
'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
|
|
4988
|
+
'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
|
|
4989
|
+
].join("\n"),
|
|
4990
|
+
reminders: ["Verify staged set is intact after pre-commit hooks."]
|
|
4991
|
+
});
|
|
4992
|
+
}
|
|
4993
|
+
if (action === "precommit_passed") {
|
|
4994
|
+
if (!checkpoint || checkpoint === null) {
|
|
4995
|
+
return guideResult(state, "FINALIZE", {
|
|
4996
|
+
instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
|
|
4997
|
+
reminders: []
|
|
4998
|
+
});
|
|
4999
|
+
}
|
|
5000
|
+
const stagedResult = await gitDiffCachedNames(root);
|
|
5001
|
+
if (!stagedResult.ok || stagedResult.data.length === 0) {
|
|
5002
|
+
const written2 = writeSessionSync(dir, { ...state, finalizeCheckpoint: null });
|
|
5003
|
+
return guideResult(written2, "FINALIZE", {
|
|
5004
|
+
instruction: 'Pre-commit hooks appear to have cleared the staging area. Re-stage your changes and call me with completedAction: "files_staged".',
|
|
5005
|
+
reminders: []
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "precommit_passed" });
|
|
5009
|
+
return guideResult(written, "FINALIZE", {
|
|
5010
|
+
instruction: [
|
|
5011
|
+
"Pre-commit passed. Now commit.",
|
|
5012
|
+
"",
|
|
5013
|
+
state.ticket ? `Commit with message: "feat: <description> (${state.ticket.id})"` : "Commit with a descriptive message.",
|
|
5014
|
+
"",
|
|
5015
|
+
'Call me with completedAction: "commit_done" and include the commitHash.'
|
|
5016
|
+
].join("\n"),
|
|
5017
|
+
reminders: []
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
if (action === "commit_done") {
|
|
5021
|
+
if (!checkpoint || checkpoint === null) {
|
|
5022
|
+
return guideResult(state, "FINALIZE", {
|
|
5023
|
+
instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
|
|
5024
|
+
reminders: []
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
if (checkpoint === "staged") {
|
|
5028
|
+
return guideResult(state, "FINALIZE", {
|
|
5029
|
+
instruction: 'You must pass pre-commit checks first. Call me with completedAction: "precommit_passed".',
|
|
5030
|
+
reminders: []
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
if (checkpoint === "committed") {
|
|
5034
|
+
return guideResult(state, "FINALIZE", {
|
|
5035
|
+
instruction: "Commit was already recorded. Proceeding to completion.",
|
|
5036
|
+
reminders: []
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
const commitHash = report.commitHash;
|
|
5040
|
+
if (!commitHash) {
|
|
5041
|
+
return guideResult(state, "FINALIZE", {
|
|
5042
|
+
instruction: "Missing commitHash in report. Call me again with the commit hash.",
|
|
5043
|
+
reminders: []
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
const completedTicket = state.ticket ? { id: state.ticket.id, title: state.ticket.title, commitHash, risk: state.ticket.risk } : void 0;
|
|
5047
|
+
const updated = {
|
|
5048
|
+
...state,
|
|
5049
|
+
state: "COMPLETE",
|
|
5050
|
+
previousState: "FINALIZE",
|
|
5051
|
+
finalizeCheckpoint: "committed",
|
|
5052
|
+
completedTickets: completedTicket ? [...state.completedTickets, completedTicket] : state.completedTickets,
|
|
5053
|
+
ticket: void 0
|
|
5054
|
+
};
|
|
5055
|
+
const written = writeSessionSync(dir, updated);
|
|
5056
|
+
appendEvent(dir, {
|
|
5057
|
+
rev: written.revision,
|
|
5058
|
+
type: "commit",
|
|
5059
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5060
|
+
data: { commitHash, ticketId: completedTicket?.id }
|
|
5061
|
+
});
|
|
5062
|
+
return handleReportComplete(root, dir, refreshLease(written), { completedAction: "commit_done" });
|
|
5063
|
+
}
|
|
5064
|
+
return guideResult(state, "FINALIZE", {
|
|
5065
|
+
instruction: 'Unexpected action at FINALIZE. Stage files and call with completedAction: "files_staged", or commit and call with completedAction: "commit_done".',
|
|
5066
|
+
reminders: []
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
async function handleReportComplete(root, dir, state, report) {
|
|
5070
|
+
const pressure = evaluatePressure(state);
|
|
5071
|
+
const updated = {
|
|
5072
|
+
...state,
|
|
5073
|
+
state: "COMPLETE",
|
|
5074
|
+
contextPressure: { ...state.contextPressure, level: pressure },
|
|
5075
|
+
finalizeCheckpoint: null
|
|
3724
5076
|
};
|
|
3725
|
-
|
|
5077
|
+
const ticketsDone = updated.completedTickets.length;
|
|
5078
|
+
const maxTickets = updated.config.maxTicketsPerSession;
|
|
5079
|
+
let nextState;
|
|
5080
|
+
let advice = "ok";
|
|
5081
|
+
if (pressure === "critical") {
|
|
5082
|
+
nextState = "HANDOVER";
|
|
5083
|
+
advice = "compact-now";
|
|
5084
|
+
} else if (ticketsDone >= maxTickets) {
|
|
5085
|
+
nextState = "HANDOVER";
|
|
5086
|
+
} else if (pressure === "high") {
|
|
5087
|
+
advice = "consider-compact";
|
|
5088
|
+
nextState = "PICK_TICKET";
|
|
5089
|
+
} else {
|
|
5090
|
+
nextState = "PICK_TICKET";
|
|
5091
|
+
}
|
|
5092
|
+
const { state: projectState } = await loadProject(root);
|
|
5093
|
+
const nextResult = nextTickets(projectState, 1);
|
|
5094
|
+
if (nextResult.kind !== "found") {
|
|
5095
|
+
nextState = "HANDOVER";
|
|
5096
|
+
}
|
|
5097
|
+
const transitioned = writeSessionSync(dir, {
|
|
5098
|
+
...updated,
|
|
5099
|
+
state: nextState,
|
|
5100
|
+
previousState: "COMPLETE"
|
|
5101
|
+
});
|
|
5102
|
+
if (nextState === "HANDOVER") {
|
|
5103
|
+
return guideResult(transitioned, "HANDOVER", {
|
|
5104
|
+
instruction: [
|
|
5105
|
+
`# Session Complete \u2014 ${ticketsDone} ticket(s) done`,
|
|
5106
|
+
"",
|
|
5107
|
+
"Write a session handover summarizing what was accomplished, decisions made, and what's next.",
|
|
5108
|
+
"",
|
|
5109
|
+
'Call me with completedAction: "handover_written" and include the content in handoverContent.'
|
|
5110
|
+
].join("\n"),
|
|
5111
|
+
reminders: [],
|
|
5112
|
+
transitionedFrom: "COMPLETE",
|
|
5113
|
+
contextAdvice: advice
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
const candidates = nextTickets(projectState, 5);
|
|
5117
|
+
let candidatesText = "";
|
|
5118
|
+
if (candidates.kind === "found") {
|
|
5119
|
+
candidatesText = candidates.candidates.map(
|
|
5120
|
+
(c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
|
|
5121
|
+
).join("\n");
|
|
5122
|
+
}
|
|
5123
|
+
const topCandidate = candidates.kind === "found" ? candidates.candidates[0] : null;
|
|
5124
|
+
return guideResult(transitioned, "PICK_TICKET", {
|
|
5125
|
+
instruction: [
|
|
5126
|
+
`# Ticket Complete \u2014 Continuing (${ticketsDone}/${maxTickets})`,
|
|
5127
|
+
"",
|
|
5128
|
+
"Do NOT stop. Do NOT ask the user. Continue immediately with the next ticket.",
|
|
5129
|
+
"",
|
|
5130
|
+
candidatesText,
|
|
5131
|
+
"",
|
|
5132
|
+
topCandidate ? `Pick **${topCandidate.ticket.id}** (highest priority) by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket by calling `claudestory_autonomous_guide` now:",
|
|
5133
|
+
"```json",
|
|
5134
|
+
topCandidate ? `{ "sessionId": "${transitioned.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${transitioned.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
|
|
5135
|
+
"```"
|
|
5136
|
+
].join("\n"),
|
|
5137
|
+
reminders: [
|
|
5138
|
+
"Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next ticket.",
|
|
5139
|
+
"Do NOT ask the user for confirmation.",
|
|
5140
|
+
"You are in autonomous mode \u2014 continue working until all tickets are done or the session limit is reached."
|
|
5141
|
+
],
|
|
5142
|
+
transitionedFrom: "COMPLETE",
|
|
5143
|
+
contextAdvice: advice
|
|
5144
|
+
});
|
|
3726
5145
|
}
|
|
3727
|
-
function
|
|
3728
|
-
|
|
5146
|
+
async function handleReportHandover(root, dir, state, report) {
|
|
5147
|
+
const content = report.handoverContent;
|
|
5148
|
+
if (!content) {
|
|
5149
|
+
return guideResult(state, "HANDOVER", {
|
|
5150
|
+
instruction: "Missing handoverContent. Write the handover and include it in the report.",
|
|
5151
|
+
reminders: []
|
|
5152
|
+
});
|
|
5153
|
+
}
|
|
5154
|
+
let handoverFailed = false;
|
|
5155
|
+
try {
|
|
5156
|
+
await handleHandoverCreate(content, "auto-session", "md", root);
|
|
5157
|
+
} catch (err) {
|
|
5158
|
+
handoverFailed = true;
|
|
5159
|
+
try {
|
|
5160
|
+
const fallbackPath = join7(dir, "handover-fallback.md");
|
|
5161
|
+
writeFileSync2(fallbackPath, content, "utf-8");
|
|
5162
|
+
} catch {
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
const written = writeSessionSync(dir, {
|
|
5166
|
+
...state,
|
|
5167
|
+
state: "SESSION_END",
|
|
5168
|
+
previousState: "HANDOVER",
|
|
5169
|
+
status: "completed"
|
|
5170
|
+
});
|
|
5171
|
+
appendEvent(dir, {
|
|
5172
|
+
rev: written.revision,
|
|
5173
|
+
type: "session_end",
|
|
5174
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5175
|
+
data: { ticketsCompleted: written.completedTickets.length, handoverFailed }
|
|
5176
|
+
});
|
|
5177
|
+
const ticketsDone = written.completedTickets.length;
|
|
5178
|
+
return guideResult(written, "SESSION_END", {
|
|
5179
|
+
instruction: [
|
|
5180
|
+
"# Session Complete",
|
|
5181
|
+
"",
|
|
5182
|
+
`${ticketsDone} ticket(s) completed.${handoverFailed ? " Handover creation failed \u2014 fallback saved to session directory." : " Handover written."} Session ended.`,
|
|
5183
|
+
"",
|
|
5184
|
+
written.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n")
|
|
5185
|
+
].join("\n"),
|
|
5186
|
+
reminders: [],
|
|
5187
|
+
transitionedFrom: "HANDOVER"
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
async function handleResume(root, args) {
|
|
5191
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for resume"));
|
|
5192
|
+
const info = findSessionById(root, args.sessionId);
|
|
5193
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5194
|
+
if (info.state.state !== "COMPACT") {
|
|
5195
|
+
return guideError(new Error(
|
|
5196
|
+
`Session ${args.sessionId} is not in COMPACT state (current: ${info.state.state}). Use action: "report" to continue.`
|
|
5197
|
+
));
|
|
5198
|
+
}
|
|
5199
|
+
const resumeState = info.state.preCompactState;
|
|
5200
|
+
if (!resumeState || !WORKFLOW_STATES.includes(resumeState)) {
|
|
5201
|
+
return guideError(new Error(
|
|
5202
|
+
`Session ${args.sessionId} has invalid preCompactState: ${resumeState}. Cannot resume safely.`
|
|
5203
|
+
));
|
|
5204
|
+
}
|
|
5205
|
+
const written = writeSessionSync(info.dir, {
|
|
5206
|
+
...refreshLease(info.state),
|
|
5207
|
+
state: resumeState,
|
|
5208
|
+
preCompactState: null,
|
|
5209
|
+
resumeFromRevision: null,
|
|
5210
|
+
contextPressure: { ...info.state.contextPressure, compactionCount: (info.state.contextPressure?.compactionCount ?? 0) + 1 }
|
|
5211
|
+
});
|
|
5212
|
+
return guideResult(written, resumeState, {
|
|
5213
|
+
instruction: [
|
|
5214
|
+
"# Resumed After Compact",
|
|
5215
|
+
"",
|
|
5216
|
+
`Session restored at state: **${resumeState}**.`,
|
|
5217
|
+
written.ticket ? `Working on: **${written.ticket.id}: ${written.ticket.title}**` : "No ticket in progress.",
|
|
5218
|
+
"",
|
|
5219
|
+
"Continue where you left off. Call me when you complete the current step."
|
|
5220
|
+
].join("\n"),
|
|
5221
|
+
reminders: [
|
|
5222
|
+
"Do NOT use plan mode.",
|
|
5223
|
+
"Call autonomous_guide after completing each step."
|
|
5224
|
+
]
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
async function handlePreCompact(root, args) {
|
|
5228
|
+
if (!args.sessionId) return guideError(new Error("sessionId is required for pre_compact"));
|
|
5229
|
+
const info = findSessionById(root, args.sessionId);
|
|
5230
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5231
|
+
if (info.state.state === "SESSION_END") {
|
|
5232
|
+
return guideError(new Error(`Session ${args.sessionId} is already ended and cannot be compacted.`));
|
|
5233
|
+
}
|
|
5234
|
+
if (info.state.state === "COMPACT") {
|
|
5235
|
+
return guideError(new Error(`Session ${args.sessionId} is already in COMPACT state. Call action: "resume" to continue.`));
|
|
5236
|
+
}
|
|
5237
|
+
const headResult = await gitHead(root);
|
|
5238
|
+
const written = writeSessionSync(info.dir, {
|
|
5239
|
+
...refreshLease(info.state),
|
|
5240
|
+
state: "COMPACT",
|
|
5241
|
+
previousState: info.state.state,
|
|
5242
|
+
preCompactState: info.state.state,
|
|
5243
|
+
resumeFromRevision: info.state.revision,
|
|
5244
|
+
git: {
|
|
5245
|
+
...info.state.git,
|
|
5246
|
+
expectedHead: headResult.ok ? headResult.data.hash : info.state.git.expectedHead
|
|
5247
|
+
}
|
|
5248
|
+
});
|
|
5249
|
+
try {
|
|
5250
|
+
const loadResult = await loadProject(root);
|
|
5251
|
+
const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
5252
|
+
await saveSnapshot2(root, loadResult);
|
|
5253
|
+
} catch {
|
|
5254
|
+
}
|
|
5255
|
+
return guideResult(written, "COMPACT", {
|
|
5256
|
+
instruction: [
|
|
5257
|
+
"# Ready for Compact",
|
|
5258
|
+
"",
|
|
5259
|
+
"State flushed. Run `/compact` now.",
|
|
5260
|
+
"",
|
|
5261
|
+
"After compact, call `claudestory_autonomous_guide` with:",
|
|
5262
|
+
"```json",
|
|
5263
|
+
`{ "sessionId": "${written.sessionId}", "action": "resume" }`,
|
|
5264
|
+
"```"
|
|
5265
|
+
].join("\n"),
|
|
5266
|
+
reminders: []
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5269
|
+
async function handleCancel(root, args) {
|
|
5270
|
+
if (!args.sessionId) {
|
|
5271
|
+
const active = findActiveSessionFull(root);
|
|
5272
|
+
if (!active) return guideError(new Error("No active session to cancel"));
|
|
5273
|
+
args = { ...args, sessionId: active.state.sessionId };
|
|
5274
|
+
}
|
|
5275
|
+
const info = findSessionById(root, args.sessionId);
|
|
5276
|
+
if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
|
|
5277
|
+
const written = writeSessionSync(info.dir, {
|
|
5278
|
+
...info.state,
|
|
5279
|
+
state: "SESSION_END",
|
|
5280
|
+
previousState: info.state.state,
|
|
5281
|
+
status: "completed"
|
|
5282
|
+
});
|
|
5283
|
+
appendEvent(info.dir, {
|
|
5284
|
+
rev: written.revision,
|
|
5285
|
+
type: "cancelled",
|
|
5286
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5287
|
+
data: { previousState: info.state.state }
|
|
5288
|
+
});
|
|
5289
|
+
return {
|
|
5290
|
+
content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) were completed.` }]
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
function guideResult(state, currentState, opts) {
|
|
5294
|
+
const summary = {
|
|
5295
|
+
ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
|
|
5296
|
+
risk: state.ticket?.risk ?? "unknown",
|
|
5297
|
+
completed: state.completedTickets.map((t) => t.id),
|
|
5298
|
+
currentStep: currentState,
|
|
5299
|
+
contextPressure: state.contextPressure?.level ?? "low",
|
|
5300
|
+
branch: state.git?.branch ?? null
|
|
5301
|
+
};
|
|
5302
|
+
const output = {
|
|
5303
|
+
sessionId: state.sessionId,
|
|
5304
|
+
state: currentState,
|
|
5305
|
+
transitionedFrom: opts.transitionedFrom,
|
|
5306
|
+
instruction: opts.instruction,
|
|
5307
|
+
reminders: opts.reminders ?? [],
|
|
5308
|
+
contextAdvice: opts.contextAdvice ?? "ok",
|
|
5309
|
+
sessionSummary: summary
|
|
5310
|
+
};
|
|
5311
|
+
const parts = [
|
|
5312
|
+
output.instruction,
|
|
5313
|
+
"",
|
|
5314
|
+
"---",
|
|
5315
|
+
`**Session:** ${output.sessionId}`,
|
|
5316
|
+
`**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
|
|
5317
|
+
`**Ticket:** ${summary.ticket}`,
|
|
5318
|
+
`**Risk:** ${summary.risk}`,
|
|
5319
|
+
`**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
|
|
5320
|
+
`**Pressure:** ${summary.contextPressure}`,
|
|
5321
|
+
summary.branch ? `**Branch:** ${summary.branch}` : "",
|
|
5322
|
+
output.contextAdvice !== "ok" ? `**Context:** ${output.contextAdvice}` : "",
|
|
5323
|
+
output.reminders.length > 0 ? `
|
|
5324
|
+
**Reminders:**
|
|
5325
|
+
${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
|
|
5326
|
+
].filter(Boolean);
|
|
5327
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
5328
|
+
}
|
|
5329
|
+
function guideError(err) {
|
|
5330
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5331
|
+
return {
|
|
5332
|
+
content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
|
|
5333
|
+
isError: true
|
|
5334
|
+
};
|
|
5335
|
+
}
|
|
5336
|
+
function readFileSafe(path2) {
|
|
5337
|
+
try {
|
|
5338
|
+
return readFileSync2(path2, "utf-8");
|
|
5339
|
+
} catch {
|
|
5340
|
+
return "";
|
|
5341
|
+
}
|
|
3729
5342
|
}
|
|
3730
5343
|
|
|
3731
5344
|
// src/cli/commands/phase.ts
|
|
3732
|
-
|
|
5345
|
+
init_esm_shims();
|
|
5346
|
+
init_queries();
|
|
5347
|
+
init_project_loader();
|
|
5348
|
+
init_ticket();
|
|
5349
|
+
init_issue();
|
|
5350
|
+
init_roadmap();
|
|
5351
|
+
import { join as join8, resolve as resolve6 } from "path";
|
|
3733
5352
|
var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3734
5353
|
var PHASE_ID_MAX_LENGTH = 40;
|
|
3735
5354
|
function validatePhaseId(id) {
|
|
@@ -3838,7 +5457,7 @@ function formatMcpError(code, message) {
|
|
|
3838
5457
|
async function runMcpReadTool(pinnedRoot, handler) {
|
|
3839
5458
|
try {
|
|
3840
5459
|
const { state, warnings } = await loadProject(pinnedRoot);
|
|
3841
|
-
const handoversDir =
|
|
5460
|
+
const handoversDir = join9(pinnedRoot, ".story", "handovers");
|
|
3842
5461
|
const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
|
|
3843
5462
|
const result = await handler(ctx);
|
|
3844
5463
|
if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
|
|
@@ -3902,7 +5521,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3902
5521
|
server.registerTool("claudestory_ticket_next", {
|
|
3903
5522
|
description: "Highest-priority unblocked ticket(s) with unblock impact and umbrella progress",
|
|
3904
5523
|
inputSchema: {
|
|
3905
|
-
count:
|
|
5524
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of candidates to return (default: 1)")
|
|
3906
5525
|
}
|
|
3907
5526
|
}, (args) => runMcpReadTool(
|
|
3908
5527
|
pinnedRoot,
|
|
@@ -3917,7 +5536,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3917
5536
|
server.registerTool("claudestory_handover_latest", {
|
|
3918
5537
|
description: "Content of the most recent handover document(s)",
|
|
3919
5538
|
inputSchema: {
|
|
3920
|
-
count:
|
|
5539
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of recent handovers to return (default: 1)")
|
|
3921
5540
|
}
|
|
3922
5541
|
}, (args) => runMcpReadTool(
|
|
3923
5542
|
pinnedRoot,
|
|
@@ -3932,7 +5551,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3932
5551
|
server.registerTool("claudestory_phase_tickets", {
|
|
3933
5552
|
description: "Leaf tickets for a specific phase, sorted by order",
|
|
3934
5553
|
inputSchema: {
|
|
3935
|
-
phaseId:
|
|
5554
|
+
phaseId: z9.string().describe("Phase ID (e.g. p5b, dogfood)")
|
|
3936
5555
|
}
|
|
3937
5556
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
|
|
3938
5557
|
const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === args.phaseId);
|
|
@@ -3948,9 +5567,9 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3948
5567
|
server.registerTool("claudestory_ticket_list", {
|
|
3949
5568
|
description: "List leaf tickets with optional filters",
|
|
3950
5569
|
inputSchema: {
|
|
3951
|
-
status:
|
|
3952
|
-
phase:
|
|
3953
|
-
type:
|
|
5570
|
+
status: z9.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
|
|
5571
|
+
phase: z9.string().optional().describe("Filter by phase ID"),
|
|
5572
|
+
type: z9.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
|
|
3954
5573
|
}
|
|
3955
5574
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
|
|
3956
5575
|
if (args.phase) {
|
|
@@ -3971,15 +5590,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3971
5590
|
server.registerTool("claudestory_ticket_get", {
|
|
3972
5591
|
description: "Get a ticket by ID (includes umbrella tickets)",
|
|
3973
5592
|
inputSchema: {
|
|
3974
|
-
id:
|
|
5593
|
+
id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
|
|
3975
5594
|
}
|
|
3976
5595
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleTicketGet(args.id, ctx)));
|
|
3977
5596
|
server.registerTool("claudestory_issue_list", {
|
|
3978
5597
|
description: "List issues with optional filters",
|
|
3979
5598
|
inputSchema: {
|
|
3980
|
-
status:
|
|
3981
|
-
severity:
|
|
3982
|
-
component:
|
|
5599
|
+
status: z9.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
|
|
5600
|
+
severity: z9.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low"),
|
|
5601
|
+
component: z9.string().optional().describe("Filter by component name")
|
|
3983
5602
|
}
|
|
3984
5603
|
}, (args) => runMcpReadTool(
|
|
3985
5604
|
pinnedRoot,
|
|
@@ -3988,13 +5607,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
3988
5607
|
server.registerTool("claudestory_issue_get", {
|
|
3989
5608
|
description: "Get an issue by ID",
|
|
3990
5609
|
inputSchema: {
|
|
3991
|
-
id:
|
|
5610
|
+
id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
|
|
3992
5611
|
}
|
|
3993
5612
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleIssueGet(args.id, ctx)));
|
|
3994
5613
|
server.registerTool("claudestory_handover_get", {
|
|
3995
5614
|
description: "Content of a specific handover document by filename",
|
|
3996
5615
|
inputSchema: {
|
|
3997
|
-
filename:
|
|
5616
|
+
filename: z9.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
|
|
3998
5617
|
}
|
|
3999
5618
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleHandoverGet(args.filename, ctx)));
|
|
4000
5619
|
server.registerTool("claudestory_recap", {
|
|
@@ -4003,7 +5622,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4003
5622
|
server.registerTool("claudestory_recommend", {
|
|
4004
5623
|
description: "Context-aware ranked work suggestions mixing tickets and issues",
|
|
4005
5624
|
inputSchema: {
|
|
4006
|
-
count:
|
|
5625
|
+
count: z9.number().int().min(1).max(10).optional().describe("Number of recommendations (default: 5)")
|
|
4007
5626
|
}
|
|
4008
5627
|
}, (args) => runMcpReadTool(
|
|
4009
5628
|
pinnedRoot,
|
|
@@ -4015,8 +5634,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4015
5634
|
server.registerTool("claudestory_export", {
|
|
4016
5635
|
description: "Self-contained project document for sharing",
|
|
4017
5636
|
inputSchema: {
|
|
4018
|
-
phase:
|
|
4019
|
-
all:
|
|
5637
|
+
phase: z9.string().optional().describe("Export a single phase by ID"),
|
|
5638
|
+
all: z9.boolean().optional().describe("Export entire project")
|
|
4020
5639
|
}
|
|
4021
5640
|
}, (args) => {
|
|
4022
5641
|
if (!args.phase && !args.all) {
|
|
@@ -4038,8 +5657,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4038
5657
|
server.registerTool("claudestory_handover_create", {
|
|
4039
5658
|
description: "Create a handover document from markdown content",
|
|
4040
5659
|
inputSchema: {
|
|
4041
|
-
content:
|
|
4042
|
-
slug:
|
|
5660
|
+
content: z9.string().describe("Markdown content of the handover"),
|
|
5661
|
+
slug: z9.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
|
|
4043
5662
|
}
|
|
4044
5663
|
}, (args) => {
|
|
4045
5664
|
if (!args.content?.trim()) {
|
|
@@ -4056,12 +5675,12 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4056
5675
|
server.registerTool("claudestory_ticket_create", {
|
|
4057
5676
|
description: "Create a new ticket",
|
|
4058
5677
|
inputSchema: {
|
|
4059
|
-
title:
|
|
4060
|
-
type:
|
|
4061
|
-
phase:
|
|
4062
|
-
description:
|
|
4063
|
-
blockedBy:
|
|
4064
|
-
parentTicket:
|
|
5678
|
+
title: z9.string().describe("Ticket title"),
|
|
5679
|
+
type: z9.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
|
|
5680
|
+
phase: z9.string().optional().describe("Phase ID"),
|
|
5681
|
+
description: z9.string().optional().describe("Ticket description"),
|
|
5682
|
+
blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets"),
|
|
5683
|
+
parentTicket: z9.string().regex(TICKET_ID_REGEX).optional().describe("Parent ticket ID (makes this a sub-ticket)")
|
|
4065
5684
|
}
|
|
4066
5685
|
}, (args) => runMcpWriteTool(
|
|
4067
5686
|
pinnedRoot,
|
|
@@ -4081,15 +5700,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4081
5700
|
server.registerTool("claudestory_ticket_update", {
|
|
4082
5701
|
description: "Update an existing ticket",
|
|
4083
5702
|
inputSchema: {
|
|
4084
|
-
id:
|
|
4085
|
-
status:
|
|
4086
|
-
title:
|
|
4087
|
-
type:
|
|
4088
|
-
order:
|
|
4089
|
-
description:
|
|
4090
|
-
phase:
|
|
4091
|
-
parentTicket:
|
|
4092
|
-
blockedBy:
|
|
5703
|
+
id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
|
|
5704
|
+
status: z9.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
|
|
5705
|
+
title: z9.string().optional().describe("New title"),
|
|
5706
|
+
type: z9.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
|
|
5707
|
+
order: z9.number().int().optional().describe("New sort order"),
|
|
5708
|
+
description: z9.string().optional().describe("New description"),
|
|
5709
|
+
phase: z9.string().nullable().optional().describe("New phase ID (null to clear)"),
|
|
5710
|
+
parentTicket: z9.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
|
|
5711
|
+
blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets")
|
|
4093
5712
|
}
|
|
4094
5713
|
}, (args) => runMcpWriteTool(
|
|
4095
5714
|
pinnedRoot,
|
|
@@ -4112,13 +5731,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4112
5731
|
server.registerTool("claudestory_issue_create", {
|
|
4113
5732
|
description: "Create a new issue",
|
|
4114
5733
|
inputSchema: {
|
|
4115
|
-
title:
|
|
4116
|
-
severity:
|
|
4117
|
-
impact:
|
|
4118
|
-
components:
|
|
4119
|
-
relatedTickets:
|
|
4120
|
-
location:
|
|
4121
|
-
phase:
|
|
5734
|
+
title: z9.string().describe("Issue title"),
|
|
5735
|
+
severity: z9.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
|
|
5736
|
+
impact: z9.string().describe("Impact description"),
|
|
5737
|
+
components: z9.array(z9.string()).optional().describe("Affected components"),
|
|
5738
|
+
relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
|
|
5739
|
+
location: z9.array(z9.string()).optional().describe("File locations"),
|
|
5740
|
+
phase: z9.string().optional().describe("Phase ID")
|
|
4122
5741
|
}
|
|
4123
5742
|
}, (args) => runMcpWriteTool(
|
|
4124
5743
|
pinnedRoot,
|
|
@@ -4139,17 +5758,17 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4139
5758
|
server.registerTool("claudestory_issue_update", {
|
|
4140
5759
|
description: "Update an existing issue",
|
|
4141
5760
|
inputSchema: {
|
|
4142
|
-
id:
|
|
4143
|
-
status:
|
|
4144
|
-
title:
|
|
4145
|
-
severity:
|
|
4146
|
-
impact:
|
|
4147
|
-
resolution:
|
|
4148
|
-
components:
|
|
4149
|
-
relatedTickets:
|
|
4150
|
-
location:
|
|
4151
|
-
order:
|
|
4152
|
-
phase:
|
|
5761
|
+
id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)"),
|
|
5762
|
+
status: z9.enum(ISSUE_STATUSES).optional().describe("New status: open, inprogress, resolved"),
|
|
5763
|
+
title: z9.string().optional().describe("New title"),
|
|
5764
|
+
severity: z9.enum(ISSUE_SEVERITIES).optional().describe("New severity"),
|
|
5765
|
+
impact: z9.string().optional().describe("New impact description"),
|
|
5766
|
+
resolution: z9.string().nullable().optional().describe("Resolution description (null to clear)"),
|
|
5767
|
+
components: z9.array(z9.string()).optional().describe("Affected components"),
|
|
5768
|
+
relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
|
|
5769
|
+
location: z9.array(z9.string()).optional().describe("File locations"),
|
|
5770
|
+
order: z9.number().int().optional().describe("New sort order"),
|
|
5771
|
+
phase: z9.string().nullable().optional().describe("New phase ID (null to clear)")
|
|
4153
5772
|
}
|
|
4154
5773
|
}, (args) => runMcpWriteTool(
|
|
4155
5774
|
pinnedRoot,
|
|
@@ -4174,8 +5793,8 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4174
5793
|
server.registerTool("claudestory_note_list", {
|
|
4175
5794
|
description: "List notes with optional status/tag filters",
|
|
4176
5795
|
inputSchema: {
|
|
4177
|
-
status:
|
|
4178
|
-
tag:
|
|
5796
|
+
status: z9.enum(NOTE_STATUSES).optional().describe("Filter by status: active, archived"),
|
|
5797
|
+
tag: z9.string().optional().describe("Filter by tag")
|
|
4179
5798
|
}
|
|
4180
5799
|
}, (args) => runMcpReadTool(
|
|
4181
5800
|
pinnedRoot,
|
|
@@ -4184,15 +5803,15 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4184
5803
|
server.registerTool("claudestory_note_get", {
|
|
4185
5804
|
description: "Get a note by ID",
|
|
4186
5805
|
inputSchema: {
|
|
4187
|
-
id:
|
|
5806
|
+
id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)")
|
|
4188
5807
|
}
|
|
4189
5808
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
|
|
4190
5809
|
server.registerTool("claudestory_note_create", {
|
|
4191
5810
|
description: "Create a new note",
|
|
4192
5811
|
inputSchema: {
|
|
4193
|
-
content:
|
|
4194
|
-
title:
|
|
4195
|
-
tags:
|
|
5812
|
+
content: z9.string().describe("Note content"),
|
|
5813
|
+
title: z9.string().optional().describe("Note title"),
|
|
5814
|
+
tags: z9.array(z9.string()).optional().describe("Tags for the note")
|
|
4196
5815
|
}
|
|
4197
5816
|
}, (args) => runMcpWriteTool(
|
|
4198
5817
|
pinnedRoot,
|
|
@@ -4209,11 +5828,11 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4209
5828
|
server.registerTool("claudestory_note_update", {
|
|
4210
5829
|
description: "Update an existing note",
|
|
4211
5830
|
inputSchema: {
|
|
4212
|
-
id:
|
|
4213
|
-
content:
|
|
4214
|
-
title:
|
|
4215
|
-
tags:
|
|
4216
|
-
status:
|
|
5831
|
+
id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)"),
|
|
5832
|
+
content: z9.string().optional().describe("New content"),
|
|
5833
|
+
title: z9.string().nullable().optional().describe("New title (null to clear)"),
|
|
5834
|
+
tags: z9.array(z9.string()).optional().describe("New tags (replaces existing)"),
|
|
5835
|
+
status: z9.enum(NOTE_STATUSES).optional().describe("New status: active, archived")
|
|
4217
5836
|
}
|
|
4218
5837
|
}, (args) => runMcpWriteTool(
|
|
4219
5838
|
pinnedRoot,
|
|
@@ -4233,13 +5852,13 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4233
5852
|
server.registerTool("claudestory_phase_create", {
|
|
4234
5853
|
description: "Create a new phase in the roadmap. Exactly one of after or atStart is required for positioning.",
|
|
4235
5854
|
inputSchema: {
|
|
4236
|
-
id:
|
|
4237
|
-
name:
|
|
4238
|
-
label:
|
|
4239
|
-
description:
|
|
4240
|
-
summary:
|
|
4241
|
-
after:
|
|
4242
|
-
atStart:
|
|
5855
|
+
id: z9.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
|
|
5856
|
+
name: z9.string().describe("Phase display name"),
|
|
5857
|
+
label: z9.string().describe("Phase label (e.g. 'PHASE 1')"),
|
|
5858
|
+
description: z9.string().describe("Phase description"),
|
|
5859
|
+
summary: z9.string().optional().describe("One-line summary for compact display"),
|
|
5860
|
+
after: z9.string().optional().describe("Insert after this phase ID"),
|
|
5861
|
+
atStart: z9.boolean().optional().describe("Insert at beginning of roadmap")
|
|
4243
5862
|
}
|
|
4244
5863
|
}, (args) => runMcpWriteTool(
|
|
4245
5864
|
pinnedRoot,
|
|
@@ -4263,14 +5882,40 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
4263
5882
|
pinnedRoot,
|
|
4264
5883
|
(root, format) => handleSelftest(root, format)
|
|
4265
5884
|
));
|
|
5885
|
+
server.registerTool("claudestory_autonomous_guide", {
|
|
5886
|
+
description: "Autonomous session orchestrator. Call at every decision point during autonomous mode.",
|
|
5887
|
+
inputSchema: {
|
|
5888
|
+
sessionId: z9.string().uuid().nullable().describe("Session ID (null for start action)"),
|
|
5889
|
+
action: z9.enum(["start", "report", "resume", "pre_compact", "cancel"]).describe("Action to perform"),
|
|
5890
|
+
report: z9.object({
|
|
5891
|
+
completedAction: z9.string().describe("What was completed"),
|
|
5892
|
+
ticketId: z9.string().optional().describe("Ticket ID (for ticket_picked)"),
|
|
5893
|
+
commitHash: z9.string().optional().describe("Git commit hash (for commit_done)"),
|
|
5894
|
+
handoverContent: z9.string().optional().describe("Handover markdown content"),
|
|
5895
|
+
verdict: z9.string().optional().describe("Review verdict: approve|revise|request_changes|reject"),
|
|
5896
|
+
findings: z9.array(z9.object({
|
|
5897
|
+
id: z9.string(),
|
|
5898
|
+
severity: z9.string(),
|
|
5899
|
+
category: z9.string(),
|
|
5900
|
+
description: z9.string(),
|
|
5901
|
+
disposition: z9.string()
|
|
5902
|
+
})).optional().describe("Review findings"),
|
|
5903
|
+
reviewerSessionId: z9.string().optional().describe("Codex session ID"),
|
|
5904
|
+
notes: z9.string().optional().describe("Free-text notes")
|
|
5905
|
+
}).optional().describe("Report data (required for report action)")
|
|
5906
|
+
}
|
|
5907
|
+
}, (args) => handleAutonomousGuide(pinnedRoot, args));
|
|
4266
5908
|
}
|
|
4267
5909
|
|
|
4268
5910
|
// src/core/init.ts
|
|
4269
|
-
|
|
4270
|
-
|
|
5911
|
+
init_esm_shims();
|
|
5912
|
+
init_project_loader();
|
|
5913
|
+
init_errors();
|
|
5914
|
+
import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
5915
|
+
import { join as join10, resolve as resolve7 } from "path";
|
|
4271
5916
|
async function initProject(root, options) {
|
|
4272
5917
|
const absRoot = resolve7(root);
|
|
4273
|
-
const wrapDir =
|
|
5918
|
+
const wrapDir = join10(absRoot, ".story");
|
|
4274
5919
|
let exists = false;
|
|
4275
5920
|
try {
|
|
4276
5921
|
const s = await stat2(wrapDir);
|
|
@@ -4290,10 +5935,10 @@ async function initProject(root, options) {
|
|
|
4290
5935
|
".story/ already exists. Use --force to overwrite config and roadmap."
|
|
4291
5936
|
);
|
|
4292
5937
|
}
|
|
4293
|
-
await mkdir4(
|
|
4294
|
-
await mkdir4(
|
|
4295
|
-
await mkdir4(
|
|
4296
|
-
await mkdir4(
|
|
5938
|
+
await mkdir4(join10(wrapDir, "tickets"), { recursive: true });
|
|
5939
|
+
await mkdir4(join10(wrapDir, "issues"), { recursive: true });
|
|
5940
|
+
await mkdir4(join10(wrapDir, "handovers"), { recursive: true });
|
|
5941
|
+
await mkdir4(join10(wrapDir, "notes"), { recursive: true });
|
|
4297
5942
|
const created = [
|
|
4298
5943
|
".story/config.json",
|
|
4299
5944
|
".story/roadmap.json",
|
|
@@ -4332,6 +5977,8 @@ async function initProject(root, options) {
|
|
|
4332
5977
|
};
|
|
4333
5978
|
await writeConfig(config, absRoot);
|
|
4334
5979
|
await writeRoadmap(roadmap, absRoot);
|
|
5980
|
+
const gitignorePath = join10(wrapDir, ".gitignore");
|
|
5981
|
+
await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
|
|
4335
5982
|
const warnings = [];
|
|
4336
5983
|
if (options.force && exists) {
|
|
4337
5984
|
try {
|
|
@@ -4350,11 +5997,26 @@ async function initProject(root, options) {
|
|
|
4350
5997
|
warnings
|
|
4351
5998
|
};
|
|
4352
5999
|
}
|
|
6000
|
+
var STORY_GITIGNORE_ENTRIES = ["snapshots/", "status.json", "sessions/"];
|
|
6001
|
+
async function ensureGitignoreEntries(gitignorePath, entries) {
|
|
6002
|
+
let existing = "";
|
|
6003
|
+
try {
|
|
6004
|
+
existing = await readFile4(gitignorePath, "utf-8");
|
|
6005
|
+
} catch {
|
|
6006
|
+
}
|
|
6007
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
6008
|
+
const missing = entries.filter((e) => !lines.includes(e));
|
|
6009
|
+
if (missing.length === 0) return;
|
|
6010
|
+
let content = existing;
|
|
6011
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
6012
|
+
content += missing.join("\n") + "\n";
|
|
6013
|
+
await writeFile2(gitignorePath, content, "utf-8");
|
|
6014
|
+
}
|
|
4353
6015
|
|
|
4354
6016
|
// src/mcp/index.ts
|
|
4355
6017
|
var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
4356
6018
|
var CONFIG_PATH2 = ".story/config.json";
|
|
4357
|
-
var version = "0.1.
|
|
6019
|
+
var version = "0.1.13";
|
|
4358
6020
|
function tryDiscoverRoot() {
|
|
4359
6021
|
const envRoot = process.env[ENV_VAR2];
|
|
4360
6022
|
if (envRoot) {
|
|
@@ -4365,8 +6027,8 @@ function tryDiscoverRoot() {
|
|
|
4365
6027
|
}
|
|
4366
6028
|
const resolved = resolve8(envRoot);
|
|
4367
6029
|
try {
|
|
4368
|
-
const canonical =
|
|
4369
|
-
if (
|
|
6030
|
+
const canonical = realpathSync2(resolved);
|
|
6031
|
+
if (existsSync8(join11(canonical, CONFIG_PATH2))) {
|
|
4370
6032
|
return canonical;
|
|
4371
6033
|
}
|
|
4372
6034
|
process.stderr.write(`Warning: No .story/config.json at ${canonical}
|
|
@@ -4379,7 +6041,7 @@ function tryDiscoverRoot() {
|
|
|
4379
6041
|
}
|
|
4380
6042
|
try {
|
|
4381
6043
|
const root = discoverProjectRoot();
|
|
4382
|
-
return root ?
|
|
6044
|
+
return root ? realpathSync2(root) : null;
|
|
4383
6045
|
} catch {
|
|
4384
6046
|
return null;
|
|
4385
6047
|
}
|
|
@@ -4394,14 +6056,14 @@ function registerDegradedTools(server) {
|
|
|
4394
6056
|
const degradedInit = server.registerTool("claudestory_init", {
|
|
4395
6057
|
description: "Initialize a new .story/ project in the current directory",
|
|
4396
6058
|
inputSchema: {
|
|
4397
|
-
name:
|
|
4398
|
-
type:
|
|
4399
|
-
language:
|
|
6059
|
+
name: z10.string().describe("Project name"),
|
|
6060
|
+
type: z10.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
|
|
6061
|
+
language: z10.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
|
|
4400
6062
|
}
|
|
4401
6063
|
}, async (args) => {
|
|
4402
6064
|
let result;
|
|
4403
6065
|
try {
|
|
4404
|
-
const projectRoot =
|
|
6066
|
+
const projectRoot = realpathSync2(process.cwd());
|
|
4405
6067
|
result = await initProject(projectRoot, {
|
|
4406
6068
|
name: args.name,
|
|
4407
6069
|
type: args.type,
|