deuk-agent-flow 4.0.37 → 4.2.7
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 +23 -0
- package/CHANGELOG.md +23 -0
- package/NOTICE.md +19 -0
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/core-rules/AGENTS.md +6 -4
- package/docs/npm-publish-guide.ko.md +70 -0
- package/docs/usage-guide.ko.md +2 -2
- package/package.json +18 -8
- package/scripts/cli-init-commands.mjs +48 -39
- package/scripts/cli-skill-commands.mjs +23 -12
- package/scripts/cli-ticket-command-shared.mjs +60 -0
- package/scripts/cli-ticket-commands.mjs +161 -114
- package/scripts/cli-ticket-index.mjs +41 -17
- package/scripts/cli-ticket-parser.mjs +4 -3
- package/scripts/lint-rules.mjs +1 -0
- package/scripts/smoke-npm-docker.mjs +1 -1
- package/scripts/smoke-npm-local.mjs +1 -0
- package/templates/TICKET_TEMPLATE.ko.md +3 -3
- package/templates/TICKET_TEMPLATE.md +3 -3
- package/templates/project-pilot/CONFORMANCE_GATE_TEMPLATE.md +2 -0
- package/templates/project-pilot/DRIFT_CHECKLIST.md +8 -0
- package/templates/skills/project-pilot/SKILL.md +17 -7
- package/templates/skills/safe-refactor/SKILL.md +1 -1
- package/docs/badges/npm-downloads.json +0 -8
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, copyFileSync, readdirSync, rmSync, statSync } from "fs";
|
|
2
2
|
import { hostname } from "os";
|
|
3
3
|
import { basename, join, dirname, relative, resolve } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
5
|
toSlug, toSnakeCaseKey, requireNonEmptySlug, toRepoRelativePath, toFileUri, inferRefTitleAndTopic, resolveReferencedTicketPath, toPosixPath, stringifyFrontMatter,
|
|
6
6
|
resolveDocsLanguage, inferDocsLanguageFromText, normalizeDocsLanguage, isMcpActive, withReadline, parseFrontMatter,
|
|
7
7
|
AGENT_ROOT_DIR, TICKET_SUBDIR, TICKET_DIR_NAME, TICKET_INDEX_FILENAME, WORKFLOW_MODE_EXECUTE,
|
|
8
8
|
detectConsumerTicketDir, resolveConsumerTicketRoot, loadInitConfig, computeTicketPath, normalizeWorkflowMode
|
|
9
9
|
} from "./cli-utils.mjs";
|
|
10
|
+
import {
|
|
11
|
+
readTicketDocument,
|
|
12
|
+
resolveTicketEntryOrComputedPath,
|
|
13
|
+
resolveTicketKnowledgeDir,
|
|
14
|
+
resolveTicketReportDir
|
|
15
|
+
} from "./cli-ticket-command-shared.mjs";
|
|
10
16
|
import { readTicketIndexJson, writeTicketIndexJson, syncActiveTicketId, generateTicketId, syncToPipeline } from "./cli-ticket-index.mjs";
|
|
11
17
|
import { appendTicketEntry, rebuildTicketIndexFromTopicFilesIfNeeded, updateTicketEntryStatus } from "./cli-ticket-parser.mjs";
|
|
12
18
|
import { appendInternalWorkflowEvent, buildTelemetrySummary, getTelemetryCompactSummary } from "./cli-telemetry-commands.mjs";
|
|
@@ -250,25 +256,30 @@ function hasSubstantiveSectionContent(text, scaffolds = []) {
|
|
|
250
256
|
return !scaffolds.some(phrase => normalized.includes(phrase));
|
|
251
257
|
}
|
|
252
258
|
|
|
253
|
-
function
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
259
|
+
function hasApcMarker(text, marker) {
|
|
260
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
261
|
+
const markerPattern = new RegExp(`^\\s*(?:#{1,6}\\s+)?${escapedMarker}(?:\\s|$)`);
|
|
262
|
+
return String(text || "").split("\n").some(line => markerPattern.test(line));
|
|
263
|
+
}
|
|
257
264
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
function getApcContentWithoutMarkers(text) {
|
|
266
|
+
return String(text || "")
|
|
267
|
+
.split("\n")
|
|
268
|
+
.map(line => {
|
|
269
|
+
for (const { marker } of REQUIRED_APC_MARKERS) {
|
|
270
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
271
|
+
const markerPattern = new RegExp(`^\\s*(?:#{1,6}\\s+)?${escapedMarker}\\s*`);
|
|
272
|
+
if (markerPattern.test(line)) return line.replace(markerPattern, "");
|
|
273
|
+
}
|
|
274
|
+
return line;
|
|
275
|
+
})
|
|
276
|
+
.join("\n")
|
|
277
|
+
.trim();
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
function getMissingApcFields(text) {
|
|
267
281
|
return REQUIRED_APC_MARKERS
|
|
268
|
-
.filter(({ marker }
|
|
269
|
-
const nextMarkers = REQUIRED_APC_MARKERS.slice(index + 1).map(item => item.marker);
|
|
270
|
-
return !hasSubstantiveSectionContent(getMarkerBody(text, marker, nextMarkers), []);
|
|
271
|
-
})
|
|
282
|
+
.filter(({ marker }) => !hasApcMarker(text, marker))
|
|
272
283
|
.map(field => field.name);
|
|
273
284
|
}
|
|
274
285
|
|
|
@@ -288,6 +299,9 @@ function getPhase1PlanBodyReasons(body) {
|
|
|
288
299
|
for (const field of getMissingApcFields(apcSection)) {
|
|
289
300
|
reasons.push(`apc_${field}_missing`);
|
|
290
301
|
}
|
|
302
|
+
if (!hasSubstantiveSectionContent(getApcContentWithoutMarkers(apcSection), [])) {
|
|
303
|
+
reasons.push("apc_content_incomplete");
|
|
304
|
+
}
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
for (const sectionName of REQUIRED_PHASE1_DATA_SECTIONS) {
|
|
@@ -312,7 +326,7 @@ function buildPlanBodyRequiredMessage(reasons = []) {
|
|
|
312
326
|
"Use the AGENTS.md self-serve recipe: do not ask the user, call help, or search for templates.",
|
|
313
327
|
"Run with stdin: `deuk-agent-flow ticket create --topic <topic> --summary \"<summary>\" --plan-body-file - --non-interactive`.",
|
|
314
328
|
"Use these exact H2 headings: `## Agent Permission Contract (APC)`, `## Compact Plan`, `## Problem Analysis`, `## Source Observations`, `## Cause Hypotheses`, `## Improvement Direction`, `## Audit Evidence`.",
|
|
315
|
-
"Under `## Agent Permission Contract (APC)`,
|
|
329
|
+
"Under `## Agent Permission Contract (APC)`, use `### [BOUNDARY]`, `### [CONTRACT]`, and `### [PATCH PLAN]`; write each body on following lines.",
|
|
316
330
|
"If a scratch plan-body file is unavoidable, keep it outside the workspace, delete it after create, and never present it as a ticket artifact.",
|
|
317
331
|
"Do not rely on template defaults or auto-generated filler text for Phase 1 ticket content."
|
|
318
332
|
].join("\n");
|
|
@@ -757,8 +771,13 @@ function rollbackTicketLifecycleArtifacts(cwd, previousIndex, previousBody, absP
|
|
|
757
771
|
}
|
|
758
772
|
|
|
759
773
|
function getPhase1IncompleteReasonsFromBody(body) {
|
|
760
|
-
|
|
761
|
-
|
|
774
|
+
let parsed = { meta: {}, content: "" };
|
|
775
|
+
try {
|
|
776
|
+
parsed = parseFrontMatter(body);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
return ["frontmatter_parse_failed"];
|
|
779
|
+
}
|
|
780
|
+
return getPhase1PlanBodyReasons(normalizePhase1PlanBodyHeadings(parsed.content || ""));
|
|
762
781
|
}
|
|
763
782
|
|
|
764
783
|
function getPhase1IncompleteReasons(cwd, absPath) {
|
|
@@ -981,18 +1000,53 @@ function buildOpenTicketLimitError(indexJson) {
|
|
|
981
1000
|
return lines.join("\n");
|
|
982
1001
|
}
|
|
983
1002
|
|
|
1003
|
+
function topicKeysForEntry(entry = {}) {
|
|
1004
|
+
const keys = [entry.topic, entry.id];
|
|
1005
|
+
try {
|
|
1006
|
+
if (entry.title) keys.push(toSlug(entry.title));
|
|
1007
|
+
} catch {
|
|
1008
|
+
// Non-ASCII or otherwise unsluggable titles are not duplicate keys.
|
|
1009
|
+
}
|
|
1010
|
+
return keys
|
|
1011
|
+
.map(value => String(value || "").toLowerCase())
|
|
1012
|
+
.filter(Boolean);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function findReusableCompletedTicket(indexJson, topic, opts = {}) {
|
|
1016
|
+
const key = String(topic || "").toLowerCase();
|
|
1017
|
+
if (!key) return null;
|
|
1018
|
+
return filterTicketEntries(indexJson.entries, opts)
|
|
1019
|
+
.filter(entry => !OPEN_TICKET_STATUSES.has(String(entry.status || "open").toLowerCase()))
|
|
1020
|
+
.sort((a, b) => String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || "")))
|
|
1021
|
+
.find(entry => topicKeysForEntry(entry).includes(key)) || null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function buildReusableTicketCreateError(entry, topic) {
|
|
1025
|
+
const id = entry?.id || entry?.topic || topic;
|
|
1026
|
+
const status = entry?.status || "closed";
|
|
1027
|
+
return [
|
|
1028
|
+
`[DUPLICATE TICKET BLOCKED] A ${status} ticket already matches "${topic}".`,
|
|
1029
|
+
`Existing ticket: ${id}`,
|
|
1030
|
+
"Do not guess .deuk-agent/tickets/sub paths for closed or archived tickets.",
|
|
1031
|
+
`Use: deuk-agent-flow ticket status --topic ${id} --status-detail --non-interactive`,
|
|
1032
|
+
`Or: deuk-agent-flow ticket use --topic ${id} --non-interactive`,
|
|
1033
|
+
"Create a new ticket only when the scope is genuinely different; use a distinct topic for that scope."
|
|
1034
|
+
].join("\n");
|
|
1035
|
+
}
|
|
1036
|
+
|
|
984
1037
|
function resolveArchiveReport(cwd, fileName, report) {
|
|
985
1038
|
if (report) return resolve(cwd, report);
|
|
986
1039
|
|
|
987
|
-
const reportDir =
|
|
1040
|
+
const reportDir = resolveTicketReportDir(cwd);
|
|
988
1041
|
const potentialReport = fileName.replace(/\.md$/i, "-report.md");
|
|
989
1042
|
const potentialPath = join(reportDir, potentialReport);
|
|
990
1043
|
return existsSync(potentialPath) ? potentialPath : null;
|
|
991
1044
|
}
|
|
992
1045
|
|
|
993
1046
|
function archiveTicketEntry({ cwd, ticketDir, indexJson, found, opts = {}, report }) {
|
|
994
|
-
const absPath =
|
|
995
|
-
const fileName = found.path.split(/[/\\]/).pop();
|
|
1047
|
+
const absPath = resolveTicketEntryOrComputedPath(cwd, found);
|
|
1048
|
+
const fileName = (found.path || computeTicketPath(found)).split(/[/\\]/).pop();
|
|
1049
|
+
const resolvedRelPath = found.path || computeTicketPath(found);
|
|
996
1050
|
if (!existsSync(absPath)) {
|
|
997
1051
|
const archivedAbsPath = findExistingArchivedTicketPath(ticketDir, found, fileName);
|
|
998
1052
|
if (archivedAbsPath) {
|
|
@@ -1027,7 +1081,7 @@ function archiveTicketEntry({ cwd, ticketDir, indexJson, found, opts = {}, repor
|
|
|
1027
1081
|
}
|
|
1028
1082
|
return { id: found.id, path: archivedRelativePath, normalized: true };
|
|
1029
1083
|
}
|
|
1030
|
-
throw new Error("ticket archive: file not found " +
|
|
1084
|
+
throw new Error("ticket archive: file not found " + resolvedRelPath);
|
|
1031
1085
|
}
|
|
1032
1086
|
|
|
1033
1087
|
const originalBody = readFileSync(absPath, "utf8");
|
|
@@ -1039,11 +1093,11 @@ function archiveTicketEntry({ cwd, ticketDir, indexJson, found, opts = {}, repor
|
|
|
1039
1093
|
const reportSrc = resolveArchiveReport(cwd, fileName, report);
|
|
1040
1094
|
let reportDest = null;
|
|
1041
1095
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1096
|
+
if (reportSrc) {
|
|
1097
|
+
if (!existsSync(reportSrc)) {
|
|
1098
|
+
throw new Error("ticket archive: report file not found " + report);
|
|
1099
|
+
}
|
|
1100
|
+
const reportDir = resolveTicketReportDir(cwd);
|
|
1047
1101
|
if (!opts.dryRun) mkdirSync(reportDir, { recursive: true });
|
|
1048
1102
|
|
|
1049
1103
|
const reportBaseName = fileName.replace(/\.md$/i, "-report.md");
|
|
@@ -1213,20 +1267,25 @@ export async function runTicketCreate(opts) {
|
|
|
1213
1267
|
}
|
|
1214
1268
|
|
|
1215
1269
|
const indexJson = readTicketIndexJson(opts.cwd);
|
|
1270
|
+
const reusableTicket = findReusableCompletedTicket(indexJson, finalTopic, opts);
|
|
1271
|
+
if (reusableTicket) {
|
|
1272
|
+
throw new Error(buildReusableTicketCreateError(reusableTicket, finalTopic));
|
|
1273
|
+
}
|
|
1216
1274
|
|
|
1217
1275
|
// Smart close: check previous active ticket's completion state before deciding
|
|
1218
1276
|
const activeId = indexJson.activeTicketId;
|
|
1219
1277
|
if (activeId) {
|
|
1220
1278
|
const activeEntry = indexJson.entries.find(e => e.id === activeId && (e.status === "open" || e.status === "active"));
|
|
1221
1279
|
if (activeEntry) {
|
|
1222
|
-
const
|
|
1280
|
+
const activeDoc = readTicketDocument(opts.cwd, activeEntry, { action: "ticket create", requireExists: false });
|
|
1281
|
+
const absPath = activeDoc.absPath;
|
|
1223
1282
|
let shouldClose = false;
|
|
1224
1283
|
let reason = "";
|
|
1225
1284
|
|
|
1226
|
-
if (
|
|
1285
|
+
if (activeDoc.exists) {
|
|
1227
1286
|
try {
|
|
1228
|
-
const body =
|
|
1229
|
-
const { meta, content } =
|
|
1287
|
+
const body = activeDoc.body;
|
|
1288
|
+
const { meta, content } = activeDoc;
|
|
1230
1289
|
|
|
1231
1290
|
// Count checklist items
|
|
1232
1291
|
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
@@ -1283,7 +1342,7 @@ export async function runTicketCreate(opts) {
|
|
|
1283
1342
|
|
|
1284
1343
|
|
|
1285
1344
|
|
|
1286
|
-
const ticketId = generateTicketId(finalTopic, indexJson
|
|
1345
|
+
const ticketId = generateTicketId(finalTopic, indexJson);
|
|
1287
1346
|
const finalFileName = `${ticketId}.md`;
|
|
1288
1347
|
|
|
1289
1348
|
const abs = join(ticketDir, group, finalFileName);
|
|
@@ -1480,11 +1539,10 @@ export async function runTicketStatus(opts) {
|
|
|
1480
1539
|
const found = pickTicketEntry(opts, index);
|
|
1481
1540
|
if (!found) throw new Error("ticket status: no matching ticket found");
|
|
1482
1541
|
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
if (!fileMissing) assertTicketLifecycleProvenance(found, parsed.meta);
|
|
1542
|
+
const ticketDoc = readTicketDocument(opts.cwd, found, { action: "ticket status", requireExists: false });
|
|
1543
|
+
const absPath = ticketDoc.absPath;
|
|
1544
|
+
const parsed = { meta: ticketDoc.meta, content: ticketDoc.content };
|
|
1545
|
+
if (ticketDoc.exists) assertTicketLifecycleProvenance(found, parsed.meta);
|
|
1488
1546
|
const phase = Number(parsed.meta.phase || 1);
|
|
1489
1547
|
const incompleteReasons = getPhase1IncompleteReasons(opts.cwd, absPath);
|
|
1490
1548
|
const derivedStatus = incompleteReasons.length > 0 && phase === 1
|
|
@@ -1494,7 +1552,7 @@ export async function runTicketStatus(opts) {
|
|
|
1494
1552
|
const out = {
|
|
1495
1553
|
id: found.id,
|
|
1496
1554
|
title: found.title,
|
|
1497
|
-
path:
|
|
1555
|
+
path: toRepoRelativePath(opts.cwd, ticketDoc.absPath),
|
|
1498
1556
|
phase,
|
|
1499
1557
|
status: derivedStatus,
|
|
1500
1558
|
summary: parsed.meta.summary || null,
|
|
@@ -1538,16 +1596,15 @@ export async function runTicketGuard(opts) {
|
|
|
1538
1596
|
throw new Error("ticket guard: no matching durable ticket found; do not call set_workflow_context.");
|
|
1539
1597
|
}
|
|
1540
1598
|
|
|
1541
|
-
const
|
|
1542
|
-
if (!
|
|
1599
|
+
const ticketDoc = readTicketDocument(opts.cwd, found, { action: "ticket guard", requireExists: false });
|
|
1600
|
+
if (!ticketDoc.exists) {
|
|
1543
1601
|
throw new Error(`ticket guard: durable ticket file missing for ${found.id || found.topic}; do not call set_workflow_context.`);
|
|
1544
1602
|
}
|
|
1603
|
+
const { meta: parsedMeta } = ticketDoc;
|
|
1604
|
+
const absPath = ticketDoc.absPath;
|
|
1605
|
+
assertTicketLifecycleProvenance(found, parsedMeta);
|
|
1545
1606
|
|
|
1546
|
-
const
|
|
1547
|
-
const parsed = parseFrontMatter(body);
|
|
1548
|
-
assertTicketLifecycleProvenance(found, parsed.meta);
|
|
1549
|
-
|
|
1550
|
-
const phase = Number(parsed.meta.phase || 1);
|
|
1607
|
+
const phase = Number(parsedMeta.phase || 1);
|
|
1551
1608
|
const reasons = getPhase1IncompleteReasons(opts.cwd, absPath);
|
|
1552
1609
|
if (phase === 1 && reasons.length > 0) {
|
|
1553
1610
|
throw new Error(`[VALIDATION FAILED] ticket guard rejected incomplete Phase 1 for ${found.id || found.topic}: ${reasons.join(", ")}. Do not call set_workflow_context until the durable ticket is complete.`);
|
|
@@ -1566,8 +1623,8 @@ export async function runTicketGuard(opts) {
|
|
|
1566
1623
|
id: found.id,
|
|
1567
1624
|
topic: found.topic,
|
|
1568
1625
|
phase,
|
|
1569
|
-
status:
|
|
1570
|
-
path:
|
|
1626
|
+
status: parsedMeta.status || found.status || "open",
|
|
1627
|
+
path: toRepoRelativePath(opts.cwd, absPath)
|
|
1571
1628
|
};
|
|
1572
1629
|
|
|
1573
1630
|
if (opts.json) {
|
|
@@ -1585,10 +1642,10 @@ export async function runTicketHandoff(opts) {
|
|
|
1585
1642
|
const current = pickTicketEntry(opts, index);
|
|
1586
1643
|
if (!current) throw new Error("ticket handoff: no matching ticket found");
|
|
1587
1644
|
|
|
1588
|
-
const
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1591
|
-
const currentParsed = currentMissing ? { meta: {}, content: "" } :
|
|
1645
|
+
const currentDoc = readTicketDocument(opts.cwd, current, { action: "ticket handoff", requireExists: false });
|
|
1646
|
+
const currentAbs = currentDoc.absPath;
|
|
1647
|
+
const currentMissing = !currentDoc.exists;
|
|
1648
|
+
const currentParsed = currentMissing ? { meta: {}, content: "" } : { meta: currentDoc.meta, content: currentDoc.content };
|
|
1592
1649
|
const currentPhase = Number(currentParsed.meta.phase || 1);
|
|
1593
1650
|
const currentReasons = currentMissing ? ["ticket_file_missing"] : getPhase1IncompleteReasons(opts.cwd, currentAbs);
|
|
1594
1651
|
const currentStatus = currentReasons.length > 0 && currentPhase === 1
|
|
@@ -1686,9 +1743,7 @@ export async function runTicketEvidenceCheck(opts) {
|
|
|
1686
1743
|
throw new Error("ticket evidence: no matching ticket found.");
|
|
1687
1744
|
}
|
|
1688
1745
|
|
|
1689
|
-
const absPath =
|
|
1690
|
-
if (!existsSync(absPath)) throw new Error("Ticket file not found: " + target.path);
|
|
1691
|
-
const { meta, content } = parseFrontMatter(readFileSync(absPath, "utf8"));
|
|
1746
|
+
const { absPath, meta, content } = readTicketDocument(opts.cwd, target, { action: "ticket evidence", requireExists: true });
|
|
1692
1747
|
const result = getClaimEvidenceResult(target, meta, content, opts.claim);
|
|
1693
1748
|
const implementationGuard = getImplementationClaimGuardResult(opts.cwd, { claim: opts.claim, content, changedFiles: opts.changedFiles });
|
|
1694
1749
|
|
|
@@ -1717,9 +1772,7 @@ export async function runTicketEvidenceReport(opts) {
|
|
|
1717
1772
|
throw new Error("ticket report: no matching ticket found.");
|
|
1718
1773
|
}
|
|
1719
1774
|
|
|
1720
|
-
const absPath =
|
|
1721
|
-
if (!existsSync(absPath)) throw new Error("Ticket file not found: " + target.path);
|
|
1722
|
-
const { meta, content } = parseFrontMatter(readFileSync(absPath, "utf8"));
|
|
1775
|
+
const { absPath, meta, content } = readTicketDocument(opts.cwd, target, { action: "ticket report", requireExists: true });
|
|
1723
1776
|
const result = getClaimEvidenceResult(target, meta, content, opts.claim);
|
|
1724
1777
|
const implementationGuard = getImplementationClaimGuardResult(opts.cwd, { claim: opts.claim, content, changedFiles: opts.changedFiles });
|
|
1725
1778
|
|
|
@@ -1772,10 +1825,8 @@ export async function runTicketClose(opts) {
|
|
|
1772
1825
|
throw new Error("No matching ticket found to update status");
|
|
1773
1826
|
}
|
|
1774
1827
|
|
|
1775
|
-
const abs =
|
|
1776
|
-
|
|
1777
|
-
const previousBody = readFileSync(abs, "utf8");
|
|
1778
|
-
const parsedForClose = parseFrontMatter(previousBody);
|
|
1828
|
+
const { absPath: abs, meta: closeMeta, body: previousBody, content: closeContent } = readTicketDocument(opts.cwd, targetEntry, { action: "ticket close", requireExists: true });
|
|
1829
|
+
const parsedForClose = { meta: closeMeta, content: closeContent };
|
|
1779
1830
|
const closePlanningReasons = getCloseLifecycleReasons(parsedForClose.meta, parsedForClose.content);
|
|
1780
1831
|
const implementationGuard = getImplementationClaimGuardResult(opts.cwd, { content: parsedForClose.content, changedFiles: opts.changedFiles });
|
|
1781
1832
|
if (!implementationGuard.ok) {
|
|
@@ -1808,7 +1859,7 @@ export async function runTicketClose(opts) {
|
|
|
1808
1859
|
syncActiveTicketId(opts.cwd);
|
|
1809
1860
|
}
|
|
1810
1861
|
|
|
1811
|
-
const finalPath = archiveResult?.path || entry
|
|
1862
|
+
const finalPath = archiveResult?.path || computeTicketPath(entry);
|
|
1812
1863
|
appendTelemetryEvent(opts.cwd, {
|
|
1813
1864
|
event: "ticket_closed",
|
|
1814
1865
|
action: "ticket-close",
|
|
@@ -1868,25 +1919,23 @@ export async function runTicketUse(opts) {
|
|
|
1868
1919
|
throw new Error(buildUseNoMatchError(targetTopic, candidates));
|
|
1869
1920
|
}
|
|
1870
1921
|
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
const foundParsed = parseFrontMatter(readFileSync(foundAbsPath, "utf8"));
|
|
1874
|
-
assertTicketLifecycleProvenance(found, foundParsed.meta);
|
|
1922
|
+
const foundDoc = readTicketDocument(opts.cwd, found, { action: "ticket use", requireExists: true });
|
|
1923
|
+
assertTicketLifecycleProvenance(found, foundDoc.meta);
|
|
1875
1924
|
|
|
1876
1925
|
// Explicitly set activeTicketId to the selected ticket
|
|
1877
1926
|
if (index.activeTicketId !== found.id) {
|
|
1878
1927
|
writeTicketIndexJson(opts.cwd, { ...index, activeTicketId: found.id });
|
|
1879
1928
|
}
|
|
1880
1929
|
|
|
1881
|
-
const absPath = toPosixPath(
|
|
1930
|
+
const absPath = toPosixPath(foundDoc.absPath);
|
|
1882
1931
|
if (isCompactTicketOutput(opts) || opts.pathOnly) {
|
|
1883
1932
|
printTicketSelectionLine(found.id, absPath, opts);
|
|
1884
1933
|
} else {
|
|
1885
|
-
const posixPath =
|
|
1934
|
+
const posixPath = toRepoRelativePath(opts.cwd, foundDoc.absPath);
|
|
1886
1935
|
console.log(`Active ticket: ${found.id}`);
|
|
1887
1936
|
console.log(`Path: [${posixPath}](file://${absPath})`);
|
|
1888
1937
|
printTicketStartLine(found.id, absPath);
|
|
1889
|
-
if (opts.printContent) console.log("\n" + readFileSync(
|
|
1938
|
+
if (opts.printContent) console.log("\n" + readFileSync(foundDoc.absPath, "utf8"));
|
|
1890
1939
|
printUsageReminder(opts.cwd, opts);
|
|
1891
1940
|
}
|
|
1892
1941
|
}
|
|
@@ -1920,7 +1969,7 @@ function distillKnowledge(absPath, ticketId, cwd, sourceBody = null) {
|
|
|
1920
1969
|
"Design Decisions",
|
|
1921
1970
|
"Analysis & Constraints"
|
|
1922
1971
|
]);
|
|
1923
|
-
const knowledgeDir =
|
|
1972
|
+
const knowledgeDir = resolveTicketKnowledgeDir(cwd);
|
|
1924
1973
|
if (!existsSync(knowledgeDir)) mkdirSync(knowledgeDir, { recursive: true });
|
|
1925
1974
|
|
|
1926
1975
|
const dest = join(knowledgeDir, `${ticketId}.json`);
|
|
@@ -2034,12 +2083,12 @@ export async function runTicketArchive(opts) {
|
|
|
2034
2083
|
const found = pickTicketEntry(opts, indexJson);
|
|
2035
2084
|
if (!found) throw new Error("ticket archive: no matching entry");
|
|
2036
2085
|
|
|
2037
|
-
const fileName = found.path.split(/[/\\]/).pop();
|
|
2086
|
+
const fileName = (found.path || computeTicketPath(found)).split(/[/\\]/).pop();
|
|
2038
2087
|
|
|
2039
2088
|
// Auto-search for report if not provided
|
|
2040
2089
|
let report = opts.report;
|
|
2041
2090
|
if (!opts.report) {
|
|
2042
|
-
const reportDir =
|
|
2091
|
+
const reportDir = resolveTicketReportDir(opts.cwd);
|
|
2043
2092
|
const potentialReport = fileName.replace(/\.md$/i, "-report.md");
|
|
2044
2093
|
const potentialPath = join(reportDir, potentialReport);
|
|
2045
2094
|
if (existsSync(potentialPath)) {
|
|
@@ -2089,8 +2138,9 @@ export async function runTicketDiscard(opts) {
|
|
|
2089
2138
|
const found = pickTicketEntry(opts, indexJson);
|
|
2090
2139
|
if (!found) throw new Error("ticket discard: no matching entry");
|
|
2091
2140
|
|
|
2092
|
-
const absPath =
|
|
2093
|
-
|
|
2141
|
+
const absPath = resolveTicketEntryOrComputedPath(opts.cwd, found);
|
|
2142
|
+
const relativePath = toRepoRelativePath(opts.cwd, absPath);
|
|
2143
|
+
if (!existsSync(absPath)) throw new Error("Ticket file not found: " + relativePath);
|
|
2094
2144
|
|
|
2095
2145
|
const body = readFileSync(absPath, "utf8");
|
|
2096
2146
|
const { meta } = parseFrontMatter(body);
|
|
@@ -2101,8 +2151,8 @@ export async function runTicketDiscard(opts) {
|
|
|
2101
2151
|
}
|
|
2102
2152
|
|
|
2103
2153
|
if (opts.dryRun) {
|
|
2104
|
-
console.log(`ticket discard: would delete ${found.id || found.topic} (${
|
|
2105
|
-
return { id: found.id, path:
|
|
2154
|
+
console.log(`ticket discard: would delete ${found.id || found.topic} (${relativePath})`);
|
|
2155
|
+
return { id: found.id, path: relativePath, discarded: false };
|
|
2106
2156
|
}
|
|
2107
2157
|
|
|
2108
2158
|
rmSync(absPath, { force: true });
|
|
@@ -2117,19 +2167,19 @@ export async function runTicketDiscard(opts) {
|
|
|
2117
2167
|
event: "ticket_discarded",
|
|
2118
2168
|
action: "ticket-discard",
|
|
2119
2169
|
ticket: found.id || found.topic,
|
|
2120
|
-
file:
|
|
2170
|
+
file: relativePath,
|
|
2121
2171
|
phase,
|
|
2122
2172
|
status: "discarded"
|
|
2123
2173
|
});
|
|
2124
2174
|
console.log(`ticket: discarded -> ${found.id || found.topic}`);
|
|
2125
|
-
return { id: found.id, path:
|
|
2175
|
+
return { id: found.id, path: relativePath, discarded: true };
|
|
2126
2176
|
}
|
|
2127
2177
|
|
|
2128
2178
|
export async function runTicketReports(opts) {
|
|
2129
2179
|
applyTicketRootContext(opts);
|
|
2130
2180
|
const ticketDir = detectConsumerTicketDir(opts.cwd);
|
|
2131
2181
|
if (!ticketDir) throw new Error("No ticket system found.");
|
|
2132
|
-
const reportDir =
|
|
2182
|
+
const reportDir = resolveTicketReportDir(opts.cwd);
|
|
2133
2183
|
console.log("\n📄 Agent Reports:");
|
|
2134
2184
|
if (!existsSync(reportDir)) {
|
|
2135
2185
|
console.log(" No reports found.");
|
|
@@ -2164,16 +2214,15 @@ export async function runTicketReportAttach(opts) {
|
|
|
2164
2214
|
const found = pickTicketEntry(opts, index);
|
|
2165
2215
|
if (!found) throw new Error("ticket report attach: no matching ticket found");
|
|
2166
2216
|
|
|
2167
|
-
const absTicketPath =
|
|
2168
|
-
if (!existsSync(absTicketPath)) throw new Error("Ticket file not found: " + found.path);
|
|
2217
|
+
const { absPath: absTicketPath } = readTicketDocument(opts.cwd, found, { action: "ticket report attach", requireExists: true });
|
|
2169
2218
|
|
|
2170
2219
|
const reportSrc = resolve(opts.cwd, opts.report);
|
|
2171
2220
|
if (!existsSync(reportSrc)) throw new Error("Report file not found: " + opts.report);
|
|
2172
2221
|
|
|
2173
|
-
const reportDir =
|
|
2222
|
+
const reportDir = resolveTicketReportDir(opts.cwd);
|
|
2174
2223
|
if (!opts.dryRun) mkdirSync(reportDir, { recursive: true });
|
|
2175
2224
|
|
|
2176
|
-
const ticketFileName = found.path.split(/[/\\]/).pop();
|
|
2225
|
+
const ticketFileName = (found.path || computeTicketPath(found)).split(/[/\\]/).pop();
|
|
2177
2226
|
const reportBaseName = ticketFileName.replace(/\.md$/i, "-report.md");
|
|
2178
2227
|
const reportDest = join(reportDir, reportBaseName);
|
|
2179
2228
|
|
|
@@ -2214,15 +2263,17 @@ export async function runTicketMove(opts) {
|
|
|
2214
2263
|
|
|
2215
2264
|
if (!entry) throw new Error("No matching ticket found to move.");
|
|
2216
2265
|
|
|
2217
|
-
const abs =
|
|
2218
|
-
if (!existsSync(abs)) throw new Error("Ticket file not found: " + entry.path);
|
|
2266
|
+
const { absPath: abs, meta: parsedMeta, content } = readTicketDocument(opts.cwd, entry, { action: "ticket move", requireExists: true });
|
|
2219
2267
|
|
|
2220
2268
|
const previousIndex = readTicketIndexJson(opts.cwd);
|
|
2221
2269
|
const body = readFileSync(abs, "utf8");
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2270
|
+
const currentPhase = Number(parsedMeta.phase || 1);
|
|
2271
|
+
const requestedPhase = Number(opts.phase);
|
|
2272
|
+
let nextPhase = opts.next
|
|
2273
|
+
? currentPhase + 1
|
|
2274
|
+
: Number.isFinite(requestedPhase)
|
|
2275
|
+
? requestedPhase
|
|
2276
|
+
: currentPhase + 1;
|
|
2226
2277
|
|
|
2227
2278
|
if (currentPhase === 1 && nextPhase >= 2) {
|
|
2228
2279
|
const reasons = getPhase1IncompleteReasons(opts.cwd, abs);
|
|
@@ -2238,22 +2289,22 @@ export async function runTicketMove(opts) {
|
|
|
2238
2289
|
}
|
|
2239
2290
|
}
|
|
2240
2291
|
|
|
2241
|
-
|
|
2292
|
+
parsedMeta.phase = nextPhase;
|
|
2242
2293
|
if (nextPhase >= 4) {
|
|
2243
|
-
|
|
2244
|
-
} else if (nextPhase >= 2 && (!
|
|
2245
|
-
|
|
2294
|
+
parsedMeta.status = "closed";
|
|
2295
|
+
} else if (nextPhase >= 2 && (!parsedMeta.status || parsedMeta.status === "open")) {
|
|
2296
|
+
parsedMeta.status = "active";
|
|
2246
2297
|
}
|
|
2247
2298
|
|
|
2248
|
-
const newBody = stringifyFrontMatter(
|
|
2299
|
+
const newBody = stringifyFrontMatter(parsedMeta, content);
|
|
2249
2300
|
|
|
2250
2301
|
try {
|
|
2251
2302
|
writeFileSync(abs, newBody, "utf8");
|
|
2252
2303
|
|
|
2253
2304
|
// Re-sync index to reflect the status change if any
|
|
2254
2305
|
opts.topic = entry.topic;
|
|
2255
|
-
if (
|
|
2256
|
-
opts.status =
|
|
2306
|
+
if (parsedMeta.status !== entry.status) {
|
|
2307
|
+
opts.status = parsedMeta.status;
|
|
2257
2308
|
updateTicketEntryStatus(opts.cwd, opts);
|
|
2258
2309
|
} else {
|
|
2259
2310
|
rebuildTicketIndexFromTopicFilesIfNeeded(opts.cwd, { ...opts, force: true });
|
|
@@ -2269,11 +2320,11 @@ export async function runTicketMove(opts) {
|
|
|
2269
2320
|
event: "ticket_phase_moved",
|
|
2270
2321
|
action: "ticket-move",
|
|
2271
2322
|
ticket: entry.id || entry.topic,
|
|
2272
|
-
file: entry
|
|
2323
|
+
file: computeTicketPath(entry),
|
|
2273
2324
|
phase: nextPhase,
|
|
2274
|
-
status:
|
|
2325
|
+
status: parsedMeta.status
|
|
2275
2326
|
});
|
|
2276
|
-
console.log(`ticket: moved -> ${entry.topic} is now in Phase ${nextPhase} (${
|
|
2327
|
+
console.log(`ticket: moved -> ${entry.topic} is now in Phase ${nextPhase} (${parsedMeta.status})`);
|
|
2277
2328
|
printUsageReminder(opts.cwd, opts);
|
|
2278
2329
|
} catch (err) {
|
|
2279
2330
|
rollbackTicketLifecycleArtifacts(opts.cwd, previousIndex, body, abs, opts);
|
|
@@ -2298,9 +2349,9 @@ export async function runTicketNext(opts) {
|
|
|
2298
2349
|
.filter(entry => String(entry.status || "").toLowerCase() === "closed")
|
|
2299
2350
|
.sort((a, b) => String(b.updatedAt || b.createdAt || "").localeCompare(String(a.updatedAt || a.createdAt || "")))[0];
|
|
2300
2351
|
if (latestClosed) {
|
|
2301
|
-
const latestClosedPath =
|
|
2352
|
+
const latestClosedPath = resolveTicketEntryOrComputedPath(opts.cwd, latestClosed);
|
|
2302
2353
|
if (existsSync(latestClosedPath)) {
|
|
2303
|
-
const { content } =
|
|
2354
|
+
const { content } = readTicketDocument(opts.cwd, latestClosed, { action: "ticket next", computePath: true });
|
|
2304
2355
|
if (followUpDecisionMeansNoFollowUp(content)) {
|
|
2305
2356
|
const absPath = toPosixPath(latestClosedPath);
|
|
2306
2357
|
if (opts.pathOnly) {
|
|
@@ -2308,7 +2359,7 @@ export async function runTicketNext(opts) {
|
|
|
2308
2359
|
} else if (isCompactTicketOutput(opts)) {
|
|
2309
2360
|
console.log(`no-follow-up:${latestClosed.id}`);
|
|
2310
2361
|
} else {
|
|
2311
|
-
const posixPath = toPosixPath(
|
|
2362
|
+
const posixPath = toPosixPath(toRepoRelativePath(opts.cwd, latestClosedPath));
|
|
2312
2363
|
console.log(`No follow-up required after ${latestClosed.id}`);
|
|
2313
2364
|
console.log(`Path: [${posixPath}](file://${absPath})`);
|
|
2314
2365
|
}
|
|
@@ -2323,23 +2374,23 @@ export async function runTicketNext(opts) {
|
|
|
2323
2374
|
writeTicketIndexJson(opts.cwd, { ...index, activeTicketId: found.id });
|
|
2324
2375
|
}
|
|
2325
2376
|
|
|
2326
|
-
const
|
|
2377
|
+
const foundAbs = readTicketDocument(opts.cwd, found, { action: "ticket next", requireExists: true });
|
|
2378
|
+
const absPath = toPosixPath(foundAbs.absPath);
|
|
2327
2379
|
if (isCompactTicketOutput(opts) || opts.pathOnly) {
|
|
2328
2380
|
printTicketSelectionLine(found.id, absPath, opts);
|
|
2329
2381
|
} else {
|
|
2330
|
-
const posixPath = toPosixPath(
|
|
2382
|
+
const posixPath = toPosixPath(toRepoRelativePath(opts.cwd, foundAbs.absPath));
|
|
2331
2383
|
console.log(`Next ticket: ${found.id}`);
|
|
2332
2384
|
console.log(`Path: [${posixPath}](file://${absPath})`);
|
|
2333
|
-
|
|
2385
|
+
if (opts.printContent) console.log("\n" + readFileSync(foundAbs.absPath, "utf8"));
|
|
2334
2386
|
}
|
|
2335
2387
|
}
|
|
2336
2388
|
|
|
2337
2389
|
function isTicketNextRunnableCandidate(cwd, entry) {
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
if (!existsSync(absPath)) return true;
|
|
2390
|
+
const ticketDoc = readTicketDocument(cwd, entry, { action: "ticket next", computePath: true, requireExists: false });
|
|
2391
|
+
if (!ticketDoc.exists) return true;
|
|
2341
2392
|
|
|
2342
|
-
const { meta } =
|
|
2393
|
+
const { meta } = ticketDoc;
|
|
2343
2394
|
const lifecycleSource = String(meta.lifecycleSource || meta.ticketLifecycleSource || "").trim();
|
|
2344
2395
|
return lifecycleSource === "ticket-create";
|
|
2345
2396
|
}
|
|
@@ -2378,11 +2429,7 @@ export async function runTicketHotfix(opts) {
|
|
|
2378
2429
|
|
|
2379
2430
|
if (!entry) throw new Error("No matching ticket found for hotfix.");
|
|
2380
2431
|
|
|
2381
|
-
const abs =
|
|
2382
|
-
if (!existsSync(abs)) throw new Error("Ticket file not found: " + entry.path);
|
|
2383
|
-
|
|
2384
|
-
const body = readFileSync(abs, "utf8");
|
|
2385
|
-
const { meta, content } = parseFrontMatter(body);
|
|
2432
|
+
const { absPath: abs, meta, content } = readTicketDocument(opts.cwd, entry, { action: "ticket hotfix", requireExists: true });
|
|
2386
2433
|
|
|
2387
2434
|
// Force phase 2 and active status, bypassing APC checks
|
|
2388
2435
|
meta.phase = 2;
|