deuk-agent-flow 4.2.7 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ko.md +259 -0
- package/CHANGELOG.md +769 -124
- package/LICENSE +0 -0
- package/README.ko.md +97 -17
- package/README.md +108 -25
- package/bin/deuk-agent-flow.js +11 -13
- package/bin/deuk-agent-rule.js +1 -1
- package/bundled/README.md +3 -0
- package/bundled/deuk-agent-flow.vsix +0 -0
- package/core-rules/AGENTS.md +30 -120
- package/docs/architecture.ko.md +155 -2
- package/docs/architecture.md +155 -2
- package/docs/assets/agentflow-panel-skills.png +0 -0
- package/docs/assets/agentflow-panel.png +0 -0
- package/docs/how-it-works.ko.md +109 -52
- package/docs/how-it-works.md +128 -71
- package/docs/principles.ko.md +68 -68
- package/docs/principles.md +68 -68
- package/docs/usage-guide.ko.md +251 -212
- package/package.json +42 -45
- package/scripts/bundle-vscode-vsix.ts +67 -0
- package/scripts/{cli-args.mjs → cli-args.ts} +49 -12
- package/scripts/cli-init-commands.ts +99 -0
- package/scripts/cli-init-logic.ts +46 -0
- package/scripts/{cli-prompts.mjs → cli-prompts.ts} +19 -34
- package/scripts/{cli-rule-compiler.mjs → cli-rule-compiler.ts} +128 -112
- package/scripts/cli-skill-commands.ts +707 -0
- package/scripts/{cli-telemetry-commands.mjs → cli-telemetry-commands.ts} +25 -22
- package/scripts/cli-ticket-command-shared.ts +4 -0
- package/scripts/cli-ticket-commands.ts +3723 -0
- package/scripts/cli-ticket-index.ts +283 -0
- package/scripts/{cli-ticket-migration.mjs → cli-ticket-migration.ts} +44 -68
- package/scripts/cli-ticket-parser.ts +100 -0
- package/scripts/{cli-usage-commands.mjs → cli-usage-commands.ts} +325 -326
- package/scripts/cli-utils.ts +1560 -0
- package/scripts/cli.ts +695 -0
- package/scripts/{lint-md.mjs → lint-md.ts} +32 -42
- package/scripts/lint-rules.ts +203 -0
- package/scripts/{merge-logic.mjs → merge-logic.ts} +44 -44
- package/scripts/{plan-parser.mjs → plan-parser.ts} +53 -53
- package/templates/MODULE_RULE_TEMPLATE.md +11 -11
- package/templates/PROJECT_RULE.md +46 -47
- package/templates/TICKET_TEMPLATE.ko.md +48 -44
- package/templates/TICKET_TEMPLATE.md +48 -44
- package/templates/project-memory.md +19 -0
- package/templates/project-pilot/CONFORMANCE_GATE_TEMPLATE.md +25 -25
- package/templates/project-pilot/DRIFT_CHECKLIST.md +27 -27
- package/templates/project-pilot/FLOW_CONTRACT_TEMPLATE.md +26 -26
- package/templates/project-pilot/IMPLEMENTATION_MATRIX_TEMPLATE.md +30 -30
- package/templates/project-pilot/INTEGRATION_CONTRACT_TEMPLATE.md +26 -26
- package/templates/project-pilot/OWNER_MAP_TEMPLATE.md +15 -15
- package/templates/project-pilot/PROJECT_PILOT_RULE_TEMPLATE.md +34 -34
- package/templates/project-pilot/REFACTOR_CONTRACT_TEMPLATE.md +32 -32
- package/templates/project-pilot/REMEDIATION_PLAN_TEMPLATE.md +33 -33
- package/templates/rules.d/deukcontext-mcp.md +31 -31
- package/templates/rules.d/platform-coexistence.md +29 -29
- package/templates/skills/context-recall/SKILL.md +3 -1
- package/templates/skills/doc-sync/SKILL.md +111 -0
- package/templates/skills/generated-file-guard/SKILL.md +3 -1
- package/templates/skills/persona-maid/SKILL.md +65 -0
- package/templates/skills/project-pilot/SKILL.md +14 -63
- package/templates/skills/safe-refactor/SKILL.md +3 -1
- package/templates/skills/ticket-status-surface/SKILL.md +17 -0
- package/NOTICE.md +0 -19
- package/core-rules/GEMINI.md +0 -7
- package/docs/npm-publish-guide.ko.md +0 -70
- package/scripts/cli-init-commands.mjs +0 -1759
- package/scripts/cli-init-logic.mjs +0 -64
- package/scripts/cli-skill-commands.mjs +0 -212
- package/scripts/cli-ticket-command-shared.mjs +0 -60
- package/scripts/cli-ticket-commands.mjs +0 -2474
- package/scripts/cli-ticket-index.mjs +0 -322
- package/scripts/cli-ticket-parser.mjs +0 -210
- package/scripts/cli-utils.mjs +0 -602
- package/scripts/cli.mjs +0 -256
- package/scripts/lint-rules.mjs +0 -197
- package/scripts/publish-dual-npm.mjs +0 -141
- package/scripts/smoke-npm-docker.mjs +0 -102
- package/scripts/smoke-npm-local.mjs +0 -110
- package/scripts/update-download-badge.mjs +0 -103
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
-
import { basename, dirname, join, resolve } from "path";
|
|
3
|
-
import { hostname as osHostname } from "os";
|
|
4
|
-
import {
|
|
5
|
-
AGENT_ROOT_DIR, TICKET_SUBDIR, TICKET_INDEX_FILENAME,
|
|
6
|
-
requireNonEmptySlug, findFileRecursively, toRepoRelativePath, detectConsumerTicketDir, computeTicketPath, normalizeTicketGroup
|
|
7
|
-
} from "./cli-utils.mjs";
|
|
8
|
-
|
|
9
|
-
const TICKET_ARCHIVE_INDEX_FILENAME = "INDEX.archive.json";
|
|
10
|
-
const TICKET_ARCHIVE_INDEX_PREFIX = "INDEX.archive.";
|
|
11
|
-
const ARCHIVE_INDEX_RETENTION_MONTHS = 12;
|
|
12
|
-
const ARCHIVE_INDEX_MONTH_RE = /^INDEX\.archive\.(\d{4}-\d{2})\.json$/;
|
|
13
|
-
const ARCHIVE_INDEX_LEGACY_RE = /^INDEX\.archive\.json$/;
|
|
14
|
-
|
|
15
|
-
function parseNextTicketSequence(value) {
|
|
16
|
-
const raw = Number(value);
|
|
17
|
-
if (!Number.isInteger(raw) || raw < 1) return null;
|
|
18
|
-
return raw;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function maxTicketNumberFromEntries(entries = []) {
|
|
22
|
-
const newRe = /^([0-9]{3,4})-/;
|
|
23
|
-
let max = 0;
|
|
24
|
-
for (const e of (entries || [])) {
|
|
25
|
-
const id = String(e.id || '');
|
|
26
|
-
const m = id.match(newRe);
|
|
27
|
-
if (m) {
|
|
28
|
-
const n = parseInt(m[1], 10);
|
|
29
|
-
if (n > max) max = n;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return max;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeNextTicketSequence(nextTicketSequence, entries = []) {
|
|
36
|
-
const fallbackNum = maxTicketNumberFromEntries(entries);
|
|
37
|
-
const fallbackNext = fallbackNum < 9999 ? (fallbackNum + 1) : fallbackNum;
|
|
38
|
-
const parsed = parseNextTicketSequence(nextTicketSequence);
|
|
39
|
-
if (parsed === null) return fallbackNext || 1;
|
|
40
|
-
return Math.max(parsed, fallbackNext || 1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function parseArchiveMonth(value) {
|
|
44
|
-
const match = String(value || "").match(/^(\d{4})-(\d{2})$/);
|
|
45
|
-
if (!match) return null;
|
|
46
|
-
const year = Number(match[1]);
|
|
47
|
-
const month = Number(match[2]);
|
|
48
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) return null;
|
|
49
|
-
return { year, month, yearMonth: `${match[1]}-${match[2]}` };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function monthDistance(fromYearMonth, toDate = new Date()) {
|
|
53
|
-
const parsed = parseArchiveMonth(fromYearMonth);
|
|
54
|
-
if (!parsed) return null;
|
|
55
|
-
return (toDate.getUTCFullYear() * 12 + toDate.getUTCMonth()) - ((parsed.year * 12) + (parsed.month - 1));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function resolveArchivePartition(entry, now = new Date()) {
|
|
59
|
-
const fromEntry = parseArchiveMonth(entry?.archiveYearMonth);
|
|
60
|
-
if (fromEntry) {
|
|
61
|
-
return {
|
|
62
|
-
yearMonth: fromEntry.yearMonth
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const source = String(entry?.createdAt || entry?.updatedAt || "");
|
|
67
|
-
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
68
|
-
if (match) {
|
|
69
|
-
return { yearMonth: `${match[1]}-${match[2]}` };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
yearMonth: `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function shouldRetainArchiveEntry(entry, now = new Date()) {
|
|
78
|
-
const partition = resolveArchivePartition(entry, now);
|
|
79
|
-
const distance = monthDistance(partition.yearMonth, now);
|
|
80
|
-
if (distance === null) return true;
|
|
81
|
-
return distance <= ARCHIVE_INDEX_RETENTION_MONTHS;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function archiveIndexFilePath(dir, yearMonth = null) {
|
|
85
|
-
return join(dir, yearMonth ? `${TICKET_ARCHIVE_INDEX_PREFIX}${yearMonth}.json` : TICKET_ARCHIVE_INDEX_FILENAME);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function listArchiveIndexFiles(dir) {
|
|
89
|
-
if (!existsSync(dir)) return [];
|
|
90
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
91
|
-
.filter(ent => ent.isFile())
|
|
92
|
-
.map(ent => join(dir, ent.name))
|
|
93
|
-
.filter(abs => ARCHIVE_INDEX_LEGACY_RE.test(basename(abs)) || ARCHIVE_INDEX_MONTH_RE.test(basename(abs)))
|
|
94
|
-
.sort((a, b) => {
|
|
95
|
-
const aBase = basename(a);
|
|
96
|
-
const bBase = basename(b);
|
|
97
|
-
const aMatch = aBase.match(ARCHIVE_INDEX_MONTH_RE);
|
|
98
|
-
const bMatch = bBase.match(ARCHIVE_INDEX_MONTH_RE);
|
|
99
|
-
if (aMatch && !bMatch) return 1;
|
|
100
|
-
if (!aMatch && bMatch) return -1;
|
|
101
|
-
if (!aMatch && !bMatch) return aBase.localeCompare(bBase);
|
|
102
|
-
return aMatch[1].localeCompare(bMatch[1]);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parseIndexFile(absPath) {
|
|
107
|
-
if (!existsSync(absPath)) {
|
|
108
|
-
return { version: 1, updatedAt: null, activeTicketId: null, nextTicketSequence: 1, entries: [] };
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
const j = JSON.parse(readFileSync(absPath, "utf8"));
|
|
112
|
-
const entries = Array.isArray(j.entries) ? j.entries.map(e => {
|
|
113
|
-
const entry = { ...e, status: e.status || "open", group: normalizeTicketGroup(e.group, "sub") };
|
|
114
|
-
entry.path = computeTicketPath(entry);
|
|
115
|
-
return entry;
|
|
116
|
-
}) : [];
|
|
117
|
-
return {
|
|
118
|
-
version: j.version || 1,
|
|
119
|
-
updatedAt: j.updatedAt ?? null,
|
|
120
|
-
activeTicketId: j.activeTicketId ?? null,
|
|
121
|
-
nextTicketSequence: parseNextTicketSequence(j.nextTicketSequence),
|
|
122
|
-
entries
|
|
123
|
-
};
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error(`[ERROR] Failed to parse ${basename(absPath)} at ${absPath}:`, err.message);
|
|
126
|
-
return { version: 1, updatedAt: null, activeTicketId: null, nextTicketSequence: 1, entries: [], _corrupt: true };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function splitEntriesForStorage(entries = []) {
|
|
131
|
-
const activeEntries = [];
|
|
132
|
-
const archiveEntries = [];
|
|
133
|
-
for (const entry of entries) {
|
|
134
|
-
const status = String(entry?.status || "open").toLowerCase();
|
|
135
|
-
if (status === "open" || status === "active") {
|
|
136
|
-
activeEntries.push(entry);
|
|
137
|
-
} else {
|
|
138
|
-
archiveEntries.push(entry);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return { activeEntries, archiveEntries };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function partitionArchiveEntries(entries = []) {
|
|
145
|
-
const retained = [];
|
|
146
|
-
const retired = [];
|
|
147
|
-
for (const entry of entries) {
|
|
148
|
-
if (shouldRetainArchiveEntry(entry)) retained.push(entry);
|
|
149
|
-
else retired.push(entry);
|
|
150
|
-
}
|
|
151
|
-
return { retained, retired };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function mergeIndexEntries(primaryEntries = [], archiveEntries = []) {
|
|
155
|
-
const merged = new Map();
|
|
156
|
-
for (const entry of primaryEntries || []) {
|
|
157
|
-
if (entry?.id) merged.set(entry.id, entry);
|
|
158
|
-
}
|
|
159
|
-
for (const entry of archiveEntries || []) {
|
|
160
|
-
if (entry?.id) merged.set(entry.id, entry);
|
|
161
|
-
}
|
|
162
|
-
return Array.from(merged.values());
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function readTicketIndexJson(cwd) {
|
|
166
|
-
const dir = detectConsumerTicketDir(cwd);
|
|
167
|
-
if (!dir) return { version: 1, updatedAt: null, entries: [] };
|
|
168
|
-
const mainPath = join(dir, TICKET_INDEX_FILENAME);
|
|
169
|
-
const main = parseIndexFile(mainPath);
|
|
170
|
-
const archiveFiles = listArchiveIndexFiles(dir).map(parseIndexFile);
|
|
171
|
-
|
|
172
|
-
const archiveEntries = archiveFiles.flatMap(file => file.entries || []);
|
|
173
|
-
const entries = mergeIndexEntries(main.entries, archiveEntries).map(entry => {
|
|
174
|
-
const next = { ...entry, status: entry.status || "open", group: normalizeTicketGroup(entry.group, "sub") };
|
|
175
|
-
next.path = computeTicketPath(next);
|
|
176
|
-
return next;
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
version: main.version || archiveFiles[0]?.version || 1,
|
|
181
|
-
updatedAt: main.updatedAt ?? archiveFiles[0]?.updatedAt ?? null,
|
|
182
|
-
activeTicketId: main.activeTicketId ?? null,
|
|
183
|
-
nextTicketSequence: normalizeNextTicketSequence(main.nextTicketSequence, entries),
|
|
184
|
-
entries,
|
|
185
|
-
_corrupt: Boolean(main._corrupt || archiveFiles.some(file => file._corrupt))
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function writeTicketIndexJson(cwd, indexJson, opts = {}) {
|
|
190
|
-
if (indexJson._corrupt && !opts.force) {
|
|
191
|
-
console.error(`[ABORT] Refusing to overwrite potentially corrupt ${TICKET_INDEX_FILENAME}. Use --force to override.`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const dir = detectConsumerTicketDir(cwd, { createIfMissing: true });
|
|
195
|
-
const p = join(dir, TICKET_INDEX_FILENAME);
|
|
196
|
-
if (opts.dryRun) return;
|
|
197
|
-
mkdirSync(dir, { recursive: true });
|
|
198
|
-
|
|
199
|
-
const out = { ...indexJson };
|
|
200
|
-
delete out._corrupt;
|
|
201
|
-
out.nextTicketSequence = normalizeNextTicketSequence(out.nextTicketSequence, out.entries || []);
|
|
202
|
-
|
|
203
|
-
// Strip physical path snapshots before saving to enforce state-driven resolution
|
|
204
|
-
const entries = Array.isArray(out.entries) ? out.entries : [];
|
|
205
|
-
const { activeEntries, archiveEntries } = splitEntriesForStorage(entries);
|
|
206
|
-
out.entries = activeEntries.map(e => {
|
|
207
|
-
const { path, archiveDay, ...clean } = e;
|
|
208
|
-
return clean;
|
|
209
|
-
});
|
|
210
|
-
out.activeTicketId = out.activeTicketId || activeEntries.find(e => e.status === "active")?.id || activeEntries.find(e => e.status === "open")?.id || null;
|
|
211
|
-
writeFileSync(p, JSON.stringify(out, null, 2) + "\n", "utf8");
|
|
212
|
-
|
|
213
|
-
const archiveFiles = listArchiveIndexFiles(dir);
|
|
214
|
-
const retainedArchiveEntries = partitionArchiveEntries(archiveEntries);
|
|
215
|
-
const archiveBuckets = new Map();
|
|
216
|
-
|
|
217
|
-
for (const entry of retainedArchiveEntries.retained) {
|
|
218
|
-
const partition = resolveArchivePartition(entry);
|
|
219
|
-
const bucket = archiveBuckets.get(partition.yearMonth) || [];
|
|
220
|
-
bucket.push({ ...entry, archiveYearMonth: partition.yearMonth });
|
|
221
|
-
archiveBuckets.set(partition.yearMonth, bucket);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const desiredArchiveFiles = new Set();
|
|
225
|
-
for (const [yearMonth, bucket] of archiveBuckets.entries()) {
|
|
226
|
-
const archiveOut = {
|
|
227
|
-
version: out.version || 1,
|
|
228
|
-
updatedAt: out.updatedAt || new Date().toISOString(),
|
|
229
|
-
activeTicketId: null,
|
|
230
|
-
nextTicketSequence: out.nextTicketSequence,
|
|
231
|
-
entries: bucket.map(e => {
|
|
232
|
-
const { path, archiveDay, ...clean } = e;
|
|
233
|
-
return clean;
|
|
234
|
-
})
|
|
235
|
-
};
|
|
236
|
-
const archivePath = archiveIndexFilePath(dir, yearMonth);
|
|
237
|
-
desiredArchiveFiles.add(archivePath);
|
|
238
|
-
writeFileSync(archivePath, JSON.stringify(archiveOut, null, 2) + "\n", "utf8");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const filePath of archiveFiles) {
|
|
242
|
-
if (!desiredArchiveFiles.has(filePath)) {
|
|
243
|
-
unlinkSync(filePath);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (retainedArchiveEntries.retired.length > 0) {
|
|
248
|
-
console.log(`[GC] Dropped ${retainedArchiveEntries.retired.length} archived entries outside the ${ARCHIVE_INDEX_RETENTION_MONTHS}-month retention window.`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function getHostnameSlug() {
|
|
253
|
-
try {
|
|
254
|
-
const slug = osHostname().toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
255
|
-
return slug.slice(0, 8).replace(/-$/, '') || 'local';
|
|
256
|
-
} catch {
|
|
257
|
-
return 'local';
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function computeNextTicketNumber(indexState) {
|
|
262
|
-
const hostname = getHostnameSlug();
|
|
263
|
-
const maybeEntries = Array.isArray(indexState) ? indexState : indexState?.entries;
|
|
264
|
-
const parsedNext = normalizeNextTicketSequence(indexState?.nextTicketSequence, maybeEntries);
|
|
265
|
-
return { num: parsedNext, hostname };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function generateTicketId(topicSlug, indexState) {
|
|
269
|
-
const hostname = getHostnameSlug();
|
|
270
|
-
const slug = requireNonEmptySlug(topicSlug, "ticket topic");
|
|
271
|
-
const match = slug.match(/^(\d{3,4})-(.*)/);
|
|
272
|
-
if (match) {
|
|
273
|
-
const numStr = match[1];
|
|
274
|
-
const restSlug = match[2].slice(0, 32);
|
|
275
|
-
return `${numStr}-${restSlug}-${hostname}`;
|
|
276
|
-
}
|
|
277
|
-
const { num } = computeNextTicketNumber(indexState);
|
|
278
|
-
const numStr = String(num).padStart(3, '0');
|
|
279
|
-
const finalSlug = slug.slice(0, 32);
|
|
280
|
-
return `${numStr}-${finalSlug}-${hostname}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function syncActiveTicketId(cwd, opts = {}) {
|
|
284
|
-
const index = readTicketIndexJson(cwd);
|
|
285
|
-
const activeEntries = index.entries.filter(e => e.status === "active");
|
|
286
|
-
const openEntries = index.entries.filter(e => e.status === "open");
|
|
287
|
-
const newestFirst = (a, b) => String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || ""));
|
|
288
|
-
const activeEntry = activeEntries.sort(newestFirst)[0] || openEntries.sort(newestFirst)[0] || null;
|
|
289
|
-
|
|
290
|
-
const ticketDir = detectConsumerTicketDir(cwd);
|
|
291
|
-
if (!ticketDir) return;
|
|
292
|
-
if (opts.dryRun) return;
|
|
293
|
-
|
|
294
|
-
const activeId = activeEntry ? activeEntry.id : null;
|
|
295
|
-
if (index.activeTicketId !== activeId) {
|
|
296
|
-
writeTicketIndexJson(cwd, { ...index, activeTicketId: activeId });
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const legacyLatestPath = join(ticketDir, "LATEST.md");
|
|
300
|
-
const pointerPathMd = join(ticketDir, "ACTIVE_TICKET.md");
|
|
301
|
-
const pointerPathJson = join(ticketDir, "ACTIVE_TICKET.json");
|
|
302
|
-
|
|
303
|
-
for (const p of [legacyLatestPath, pointerPathMd, pointerPathJson]) {
|
|
304
|
-
if (existsSync(p)) {
|
|
305
|
-
unlinkSync(p);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export async function syncToPipeline(url, data) {
|
|
311
|
-
try {
|
|
312
|
-
const response = await fetch(url, {
|
|
313
|
-
method: "POST",
|
|
314
|
-
headers: { "Content-Type": "application/json" },
|
|
315
|
-
body: JSON.stringify(data),
|
|
316
|
-
signal: AbortSignal?.timeout ? AbortSignal.timeout(3000) : undefined
|
|
317
|
-
});
|
|
318
|
-
return response.ok;
|
|
319
|
-
} catch (err) {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
2
|
-
import { basename, dirname, join } from "path";
|
|
3
|
-
import {
|
|
4
|
-
AGENT_ROOT_DIR, TICKET_SUBDIR, TICKET_LIST_FILENAME,
|
|
5
|
-
toPosixPath, toRepoRelativePath, detectProjectFromBody, deriveTopicFromBaseName, normalizeTicketGroup,
|
|
6
|
-
parseFrontMatter, stringifyFrontMatter, discoverAllWorkspaces, detectConsumerTicketDir, computeTicketPath,
|
|
7
|
-
ARCHIVE_YEAR_MONTH_RE, ARCHIVE_DAY_RE
|
|
8
|
-
} from "./cli-utils.mjs";
|
|
9
|
-
import { readTicketIndexJson, writeTicketIndexJson } from "./cli-ticket-index.mjs";
|
|
10
|
-
|
|
11
|
-
export function collectTicketMarkdownFiles(dir, out = []) {
|
|
12
|
-
if (!existsSync(dir)) return out;
|
|
13
|
-
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
-
const abs = join(dir, ent.name);
|
|
15
|
-
if (ent.name === "node_modules" || ent.name === ".git") continue;
|
|
16
|
-
if (ent.isDirectory()) collectTicketMarkdownFiles(abs, out);
|
|
17
|
-
else if (ent.isFile() && /\.md$/i.test(ent.name)) {
|
|
18
|
-
const base = ent.name;
|
|
19
|
-
if (base === "LATEST.md" || base === TICKET_LIST_FILENAME || base === "ACTIVE_TICKET.md") continue;
|
|
20
|
-
out.push(abs);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function discoverAllTicketDirs(baseCwd, out = []) {
|
|
27
|
-
return discoverAllWorkspaces(baseCwd, undefined, new Set(out));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts = {}) {
|
|
31
|
-
const indexJson = readTicketIndexJson(cwd);
|
|
32
|
-
|
|
33
|
-
if (indexJson.entries.length > 0 && !opts.force && !opts.rebuild) {
|
|
34
|
-
return indexJson;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const ticketDir = detectConsumerTicketDir(cwd);
|
|
38
|
-
if (!ticketDir) return indexJson;
|
|
39
|
-
|
|
40
|
-
// Strictly scan only the official ticket system directory and its subdirectories
|
|
41
|
-
const ticketDirs = [ticketDir];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (ticketDirs.length === 0) return indexJson;
|
|
45
|
-
|
|
46
|
-
const files = [];
|
|
47
|
-
for (const dir of ticketDirs) {
|
|
48
|
-
collectTicketMarkdownFiles(dir, files);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let dirty = false;
|
|
52
|
-
const newEntries = [];
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < files.length; i++) {
|
|
55
|
-
const entry = processTicketFile(files[i], cwd, indexJson, opts);
|
|
56
|
-
if (entry) newEntries.push(entry);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (JSON.stringify(indexJson.entries) !== JSON.stringify(newEntries)) {
|
|
60
|
-
dirty = true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (dirty || opts.force || opts.rebuild) {
|
|
64
|
-
newEntries.sort((a,b) => String(b.createdAt||"").localeCompare(String(a.createdAt||"")));
|
|
65
|
-
const next = { version: 1, updatedAt: new Date().toISOString(), entries: newEntries };
|
|
66
|
-
writeTicketIndexJson(cwd, next, opts);
|
|
67
|
-
if (opts.rebuild) console.log(`[REBUILD] INDEX.json rebuilt with ${newEntries.length} entries.`);
|
|
68
|
-
return next;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return indexJson;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function processTicketFile(abs, cwd, indexJson, opts) {
|
|
75
|
-
const rel = toPosixPath(toRepoRelativePath(cwd, abs));
|
|
76
|
-
const filename = basename(abs);
|
|
77
|
-
const idFromFilename = filename.replace(/\.md$/i, "");
|
|
78
|
-
const isAlreadyInArchive = rel.includes("/archive/");
|
|
79
|
-
const storage = parseTicketStorage(rel, isAlreadyInArchive, abs);
|
|
80
|
-
const group = storage.group;
|
|
81
|
-
|
|
82
|
-
// Optimization: If entry already exists in index and not forced, reuse metadata to save I/O & tokens
|
|
83
|
-
const existing = indexJson.entries.find(e => e.id === idFromFilename);
|
|
84
|
-
if (existing && !opts.force) {
|
|
85
|
-
return {
|
|
86
|
-
...existing,
|
|
87
|
-
group,
|
|
88
|
-
archiveYearMonth: storage.archiveYearMonth || existing.archiveYearMonth,
|
|
89
|
-
status: isAlreadyInArchive ? "archived" : (existing.status === "archived" ? "open" : existing.status),
|
|
90
|
-
updatedAt: statSync(abs).mtime.toISOString()
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// New or forced entry: Parse file content
|
|
95
|
-
let meta = {}, content = "";
|
|
96
|
-
try {
|
|
97
|
-
const body = readFileSync(abs, "utf8");
|
|
98
|
-
const parsed = parseFrontMatter(body);
|
|
99
|
-
meta = parsed.meta;
|
|
100
|
-
content = parsed.content;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.warn(`[WARNING] Failed to parse ${rel}: ${err.message}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const title = meta.title || idFromFilename;
|
|
106
|
-
const project = meta.project || detectProjectFromBody(content);
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
id: meta.id || idFromFilename,
|
|
110
|
-
title,
|
|
111
|
-
topic: deriveTopicFromBaseName(filename),
|
|
112
|
-
group,
|
|
113
|
-
fileName: filename,
|
|
114
|
-
project,
|
|
115
|
-
submodule: meta.submodule || (rel.startsWith(AGENT_ROOT_DIR) ? "" : rel.split("/")[0]),
|
|
116
|
-
createdAt: meta.createdAt || statSync(abs).mtime.toISOString(),
|
|
117
|
-
updatedAt: statSync(abs).mtime.toISOString(),
|
|
118
|
-
source: "ticket-sync",
|
|
119
|
-
status: isAlreadyInArchive ? "archived" : (meta.status || "open"),
|
|
120
|
-
archiveYearMonth: storage.archiveYearMonth,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function parseTicketStorage(rel, isArchived, abs) {
|
|
125
|
-
if (!isArchived) {
|
|
126
|
-
return { group: basename(dirname(abs)) };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const parts = rel.split("/");
|
|
130
|
-
const archiveIdx = parts.indexOf("archive");
|
|
131
|
-
const group = parts[archiveIdx + 1] || basename(dirname(abs));
|
|
132
|
-
const maybeYearMonth = parts[archiveIdx + 2];
|
|
133
|
-
const maybeDay = parts[archiveIdx + 3];
|
|
134
|
-
|
|
135
|
-
if (ARCHIVE_YEAR_MONTH_RE.test(String(maybeYearMonth || "")) && ARCHIVE_DAY_RE.test(String(maybeDay || ""))) {
|
|
136
|
-
return { group: normalizeTicketGroup(group), archiveYearMonth: maybeYearMonth, archiveDay: maybeDay };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (ARCHIVE_YEAR_MONTH_RE.test(String(maybeYearMonth || ""))) {
|
|
140
|
-
return { group: normalizeTicketGroup(group), archiveYearMonth: maybeYearMonth };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { group: normalizeTicketGroup(group) };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function appendTicketEntry(cwd, entry, opts = {}) {
|
|
147
|
-
const indexJson = readTicketIndexJson(cwd);
|
|
148
|
-
entry.status = entry.status || "open";
|
|
149
|
-
// We no longer store 'path' snapshots in INDEX.json
|
|
150
|
-
const { path, ...cleanEntry } = entry;
|
|
151
|
-
const next = {
|
|
152
|
-
version: indexJson.version || 1,
|
|
153
|
-
updatedAt: new Date().toISOString(),
|
|
154
|
-
activeTicketId: indexJson.activeTicketId,
|
|
155
|
-
entries: [cleanEntry, ...indexJson.entries]
|
|
156
|
-
};
|
|
157
|
-
writeTicketIndexJson(cwd, next, opts);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function updateTicketEntryStatus(cwd, opts = {}) {
|
|
161
|
-
const indexJson = rebuildTicketIndexFromTopicFilesIfNeeded(cwd, opts);
|
|
162
|
-
let foundIndex = -1;
|
|
163
|
-
const targetTopic = opts.topic ? String(opts.topic).toLowerCase() : null;
|
|
164
|
-
|
|
165
|
-
if (opts.latest) {
|
|
166
|
-
foundIndex = 0;
|
|
167
|
-
} else if (targetTopic) {
|
|
168
|
-
// Match against both topic AND id for consistency with pickTicketEntry
|
|
169
|
-
foundIndex = indexJson.entries.findIndex(e =>
|
|
170
|
-
String(e.topic || "").toLowerCase().includes(targetTopic) ||
|
|
171
|
-
String(e.id || "").toLowerCase().includes(targetTopic)
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (foundIndex === -1) {
|
|
176
|
-
throw new Error("No matching ticket found to update status");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const entry = indexJson.entries[foundIndex];
|
|
180
|
-
const newStatus = opts.status || "closed";
|
|
181
|
-
entry.status = newStatus;
|
|
182
|
-
if (newStatus === "closed" || newStatus === "cancelled" || newStatus === "wontfix") {
|
|
183
|
-
entry.phase = 4;
|
|
184
|
-
}
|
|
185
|
-
entry.updatedAt = new Date().toISOString();
|
|
186
|
-
|
|
187
|
-
// Sync status back to .md frontmatter to prevent rebuild reversion
|
|
188
|
-
const entryPath = entry.path || computeTicketPath(entry);
|
|
189
|
-
const absPath = join(cwd, entryPath);
|
|
190
|
-
if (existsSync(absPath)) {
|
|
191
|
-
try {
|
|
192
|
-
const body = readFileSync(absPath, "utf8");
|
|
193
|
-
const parsed = parseFrontMatter(body);
|
|
194
|
-
if (parsed.meta.status !== newStatus) {
|
|
195
|
-
parsed.meta.status = newStatus;
|
|
196
|
-
if (newStatus === "closed" || newStatus === "cancelled" || newStatus === "wontfix") {
|
|
197
|
-
parsed.meta.phase = 4;
|
|
198
|
-
}
|
|
199
|
-
const newBody = stringifyFrontMatter(parsed.meta, parsed.content);
|
|
200
|
-
writeFileSync(absPath, newBody, "utf8");
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.warn(`[WARNING] Failed to sync status to ${entryPath}: ${err.message}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const next = { version: indexJson.version, updatedAt: new Date().toISOString(), entries: indexJson.entries };
|
|
208
|
-
writeTicketIndexJson(cwd, next, opts);
|
|
209
|
-
return entry;
|
|
210
|
-
}
|