deuk-agent-flow 4.2.7 → 5.0.4
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 +277 -0
- package/CHANGELOG.md +794 -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 +60 -45
- package/scripts/bundle-vscode-vsix.ts +67 -0
- package/scripts/{cli-args.mjs → cli-args.ts} +49 -12
- package/scripts/cli-context-commands.ts +35 -0
- package/scripts/cli-init-claude.ts +179 -0
- package/scripts/cli-init-commands.ts +99 -0
- package/scripts/cli-init-core.ts +89 -0
- package/scripts/cli-init-instructions.ts +489 -0
- package/scripts/cli-init-logic.ts +46 -0
- package/scripts/cli-init-migrate.ts +504 -0
- package/scripts/cli-init-workspace.ts +63 -0
- package/scripts/cli-locales.ts +85 -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 +709 -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 +3781 -0
- package/scripts/cli-ticket-docmeta.ts +103 -0
- package/scripts/cli-ticket-document.ts +153 -0
- package/scripts/cli-ticket-evidence.ts +312 -0
- package/scripts/cli-ticket-home.ts +449 -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-ticket-scan.ts +102 -0
- package/scripts/cli-ticket-surface.ts +56 -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/oss-prepack.js +32 -0
- package/scripts/out/scripts/bundle-vscode-vsix.js +68 -0
- package/scripts/out/scripts/bundle-vscode-vsix.js.map +1 -0
- package/scripts/out/scripts/cli-args.js +374 -0
- package/scripts/out/scripts/cli-args.js.map +1 -0
- package/scripts/out/scripts/cli-context-commands.js +31 -0
- package/scripts/out/scripts/cli-context-commands.js.map +1 -0
- package/scripts/out/scripts/cli-init-claude.js +176 -0
- package/scripts/out/scripts/cli-init-claude.js.map +1 -0
- package/scripts/out/scripts/cli-init-commands.js +87 -0
- package/scripts/out/scripts/cli-init-commands.js.map +1 -0
- package/scripts/out/scripts/cli-init-core.js +89 -0
- package/scripts/out/scripts/cli-init-core.js.map +1 -0
- package/scripts/out/scripts/cli-init-instructions.js +460 -0
- package/scripts/out/scripts/cli-init-instructions.js.map +1 -0
- package/scripts/out/scripts/cli-init-logic.js +41 -0
- package/scripts/out/scripts/cli-init-logic.js.map +1 -0
- package/scripts/out/scripts/cli-init-migrate.js +497 -0
- package/scripts/out/scripts/cli-init-migrate.js.map +1 -0
- package/scripts/out/scripts/cli-init-workspace.js +57 -0
- package/scripts/out/scripts/cli-init-workspace.js.map +1 -0
- package/scripts/out/scripts/cli-locales.js +83 -0
- package/scripts/out/scripts/cli-locales.js.map +1 -0
- package/scripts/out/scripts/cli-prompts.js +101 -0
- package/scripts/out/scripts/cli-prompts.js.map +1 -0
- package/scripts/out/scripts/cli-rule-compiler.js +121 -0
- package/scripts/out/scripts/cli-rule-compiler.js.map +1 -0
- package/scripts/out/scripts/cli-skill-commands.js +708 -0
- package/scripts/out/scripts/cli-skill-commands.js.map +1 -0
- package/scripts/out/scripts/cli-telemetry-commands.js +604 -0
- package/scripts/out/scripts/cli-telemetry-commands.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-command-shared.js +2 -0
- package/scripts/out/scripts/cli-ticket-command-shared.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-commands.js +3530 -0
- package/scripts/out/scripts/cli-ticket-commands.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-docmeta.js +90 -0
- package/scripts/out/scripts/cli-ticket-docmeta.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-document.js +121 -0
- package/scripts/out/scripts/cli-ticket-document.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-evidence.js +286 -0
- package/scripts/out/scripts/cli-ticket-evidence.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-home.js +440 -0
- package/scripts/out/scripts/cli-ticket-home.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-index.js +272 -0
- package/scripts/out/scripts/cli-ticket-index.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-migration.js +261 -0
- package/scripts/out/scripts/cli-ticket-migration.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-parser.js +90 -0
- package/scripts/out/scripts/cli-ticket-parser.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-scan.js +93 -0
- package/scripts/out/scripts/cli-ticket-scan.js.map +1 -0
- package/scripts/out/scripts/cli-ticket-surface.js +53 -0
- package/scripts/out/scripts/cli-ticket-surface.js.map +1 -0
- package/scripts/out/scripts/cli-usage-commands.js +310 -0
- package/scripts/out/scripts/cli-usage-commands.js.map +1 -0
- package/scripts/out/scripts/cli-utils.js +1544 -0
- package/scripts/out/scripts/cli-utils.js.map +1 -0
- package/scripts/out/scripts/cli.js +696 -0
- package/scripts/out/scripts/cli.js.map +1 -0
- package/scripts/out/scripts/lint-md.js +238 -0
- package/scripts/out/scripts/lint-md.js.map +1 -0
- package/scripts/out/scripts/lint-rules.js +193 -0
- package/scripts/out/scripts/lint-rules.js.map +1 -0
- package/scripts/out/scripts/merge-logic.js +38 -0
- package/scripts/out/scripts/merge-logic.js.map +1 -0
- package/scripts/out/scripts/plan-parser.js +52 -0
- package/scripts/out/scripts/plan-parser.js.map +1 -0
- package/scripts/out/scripts/ticket-machine.js +94 -0
- package/scripts/out/scripts/ticket-machine.js.map +1 -0
- package/scripts/out/scripts/ticket-workflow.js +498 -0
- package/scripts/out/scripts/ticket-workflow.js.map +1 -0
- package/scripts/{plan-parser.mjs → plan-parser.ts} +53 -53
- package/scripts/ticket-machine.ts +99 -0
- package/scripts/ticket-workflow.ts +544 -0
- 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/role-closer/SKILL.md +27 -0
- package/templates/skills/role-implementer/SKILL.md +30 -0
- package/templates/skills/role-planner/SKILL.md +29 -0
- package/templates/skills/role-qa-verifier/SKILL.md +30 -0
- 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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
|
|
3
|
+
export function splitTicketDocMetaSection(content = "") {
|
|
4
|
+
const src = String(content || "");
|
|
5
|
+
const lines = src.split(/\r?\n/);
|
|
6
|
+
const start = lines.findIndex(line => /^##\s+DocMeta\s*$/i.test(line.trim()));
|
|
7
|
+
if (start === -1) {
|
|
8
|
+
return { body: src, docmetaRaw: "", docmeta: null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const body = lines.slice(0, start).join("\n").trimEnd();
|
|
12
|
+
const docmetaSection = lines.slice(start).join("\n").trim();
|
|
13
|
+
const fence = docmetaSection.match(/```(?:ya?ml)?\s*\n([\s\S]*?)\n```/i);
|
|
14
|
+
const docmetaRaw = fence ? fence[1].trim() : "";
|
|
15
|
+
let docmeta = null;
|
|
16
|
+
if (docmetaRaw) {
|
|
17
|
+
try {
|
|
18
|
+
docmeta = YAML.parse(docmetaRaw) || null;
|
|
19
|
+
} catch {
|
|
20
|
+
docmeta = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { body, docmetaRaw, docmeta };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildDocMetaFrontmatterSummary(docmeta = null) {
|
|
27
|
+
if (!docmeta || typeof docmeta !== "object") return {};
|
|
28
|
+
const errors = Array.isArray(docmeta.validation?.errors)
|
|
29
|
+
? docmeta.validation.errors.map(value => String(value)).filter(Boolean)
|
|
30
|
+
: [];
|
|
31
|
+
return Object.fromEntries(Object.entries({
|
|
32
|
+
docmetaStatus: docmeta.output_status || undefined,
|
|
33
|
+
docmetaTarget: docmeta.target_state || docmeta.targetState || undefined,
|
|
34
|
+
docmetaValidation: docmeta.validation?.status || undefined,
|
|
35
|
+
docmetaErrors: errors.length ? errors : undefined
|
|
36
|
+
}).filter(([, value]) => value !== undefined && value !== ""));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearDocMetaFrontmatterSummary(meta: Record<string, any> = {}) {
|
|
40
|
+
const next = { ...meta };
|
|
41
|
+
delete next.docmeta;
|
|
42
|
+
delete next.docmetaStatus;
|
|
43
|
+
delete next.docmetaTarget;
|
|
44
|
+
delete next.docmetaValidation;
|
|
45
|
+
delete next.docmetaErrors;
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function appendTicketDocMetaSection(content = "", docmeta = null) {
|
|
50
|
+
const body = splitTicketDocMetaSection(content).body.trimEnd();
|
|
51
|
+
if (!docmeta || typeof docmeta !== "object") return `${body}\n`;
|
|
52
|
+
const yamlStr = YAML.stringify(docmeta).trim();
|
|
53
|
+
return `${body}\n\n## DocMeta\n\n\`\`\`yaml\n${yamlStr}\n\`\`\`\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildTransitionValidationDocMeta(input: Record<string, any> = {}) {
|
|
57
|
+
const {
|
|
58
|
+
ticketId = "",
|
|
59
|
+
phase = 1,
|
|
60
|
+
status = "open",
|
|
61
|
+
currentState = `phase${phase || 1}`,
|
|
62
|
+
targetState = "",
|
|
63
|
+
requiredSlots = [],
|
|
64
|
+
slotValues = {},
|
|
65
|
+
slotSourceMap = {}
|
|
66
|
+
} = input;
|
|
67
|
+
const slots = Object.fromEntries(requiredSlots.map(slot => [slot, Boolean(slotValues[slot])]));
|
|
68
|
+
const filteredSlotSourceMap = Object.fromEntries(
|
|
69
|
+
requiredSlots.map(slot => [slot, slotSourceMap[slot] || { source: "unknown" }])
|
|
70
|
+
);
|
|
71
|
+
const errors = Object.entries(slots)
|
|
72
|
+
.filter(([, passed]) => !passed)
|
|
73
|
+
.map(([slot]) => slot);
|
|
74
|
+
return {
|
|
75
|
+
document_type: "ticket_validation",
|
|
76
|
+
document_subtype: "ticket_workflow_transition_gate",
|
|
77
|
+
contract_version: "documeta-0.1",
|
|
78
|
+
ticket_id: ticketId,
|
|
79
|
+
state: {
|
|
80
|
+
name: currentState,
|
|
81
|
+
phase,
|
|
82
|
+
status: String(status || "open")
|
|
83
|
+
},
|
|
84
|
+
target_state: targetState,
|
|
85
|
+
source_contract: {
|
|
86
|
+
required_slots: requiredSlots,
|
|
87
|
+
metadata_fields: ["ticket_id", "workspace_id", "phase", "status", "target_state", "source_path"]
|
|
88
|
+
},
|
|
89
|
+
slots,
|
|
90
|
+
slot_source_map: filteredSlotSourceMap,
|
|
91
|
+
validation: {
|
|
92
|
+
status: errors.length === 0 ? "PASS" : "NEEDS_FIX",
|
|
93
|
+
errors
|
|
94
|
+
},
|
|
95
|
+
output_status: errors.length === 0 ? "transition_allowed" : "transition_blocked",
|
|
96
|
+
adapter_contracts: {
|
|
97
|
+
cli: {
|
|
98
|
+
action: targetState === "phase2" ? "ticket move" : "ticket workflow transition",
|
|
99
|
+
stop_if_output_status: "transition_blocked"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { basename } from "path";
|
|
3
|
+
import {
|
|
4
|
+
DEUK_ROOT_DIR,
|
|
5
|
+
computeTicketPath,
|
|
6
|
+
detectConsumerTicketDir,
|
|
7
|
+
normalizeTicketGroup,
|
|
8
|
+
parseFrontMatter,
|
|
9
|
+
stringifyFrontMatter,
|
|
10
|
+
writeFileLF,
|
|
11
|
+
makePath
|
|
12
|
+
, CliOpts } from "./cli-utils.js";
|
|
13
|
+
import { appendTicketDocMetaSection, splitTicketDocMetaSection } from "./cli-ticket-docmeta.js";
|
|
14
|
+
import { normalizeMarkdownContent } from "./lint-md.js";
|
|
15
|
+
|
|
16
|
+
export function resolveTicketPath(cwd, relPath) {
|
|
17
|
+
// #080: stale entries (old INDEX/claim/frontmatter) may still carry the legacy
|
|
18
|
+
// `.deuk-agent/tickets/` prefix from in-workspace storage. The base passed here is
|
|
19
|
+
// the ticket root itself post-#622, so strip the prefix instead of nesting.
|
|
20
|
+
let rel = String(relPath || "");
|
|
21
|
+
const legacyPrefix = `${DEUK_ROOT_DIR}/tickets/`;
|
|
22
|
+
if (rel.startsWith(legacyPrefix)) rel = rel.slice(legacyPrefix.length);
|
|
23
|
+
return makePath(cwd, rel);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveTicketEntryPath(cwd, entry) {
|
|
27
|
+
return resolveTicketPath(cwd, entry?.path || "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveTicketEntryOrComputedPath(cwd, entry) {
|
|
31
|
+
if (entry?.path) {
|
|
32
|
+
const fallbackPath = resolveTicketPath(cwd, entry.path);
|
|
33
|
+
if (existsSync(fallbackPath)) {
|
|
34
|
+
return fallbackPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const computedPath = resolveTicketPath(cwd, computeTicketPath(entry));
|
|
39
|
+
if (existsSync(computedPath)) {
|
|
40
|
+
return computedPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ticketDir = detectConsumerTicketDir(cwd);
|
|
44
|
+
if (!ticketDir) {
|
|
45
|
+
return computedPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fileName = entry?.fileName
|
|
49
|
+
? String(entry.fileName)
|
|
50
|
+
: basename(entry?.path || computeTicketPath(entry));
|
|
51
|
+
|
|
52
|
+
if (!fileName) {
|
|
53
|
+
return computedPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const group = normalizeTicketGroup(entry?.group || "sub");
|
|
57
|
+
const archivedYearMonth = String(entry?.archiveYearMonth || "").trim();
|
|
58
|
+
const archivedDay = String(entry?.archiveDay || "").trim();
|
|
59
|
+
if (String(entry?.status || "").toLowerCase() === "archived" && archivedYearMonth && archivedDay) {
|
|
60
|
+
const dayPath = makePath(
|
|
61
|
+
ticketDir,
|
|
62
|
+
"archive",
|
|
63
|
+
group,
|
|
64
|
+
archivedYearMonth,
|
|
65
|
+
archivedDay,
|
|
66
|
+
fileName
|
|
67
|
+
);
|
|
68
|
+
if (existsSync(dayPath)) {
|
|
69
|
+
return dayPath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const candidates = readdirSync(ticketDir, { withFileTypes: true });
|
|
74
|
+
const stack = [...candidates.filter(item => item.isDirectory()).map(item => makePath(ticketDir, item.name))];
|
|
75
|
+
while (stack.length > 0) {
|
|
76
|
+
const dir = stack.pop();
|
|
77
|
+
if (!existsSync(dir)) continue;
|
|
78
|
+
|
|
79
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
for (const item of entries) {
|
|
81
|
+
if (item.isDirectory()) {
|
|
82
|
+
if (item.name === ".git" || item.name === "node_modules") continue;
|
|
83
|
+
stack.push(makePath(dir, item.name));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (item.isFile() && item.name === fileName) {
|
|
87
|
+
return makePath(dir, item.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return computedPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function readTicketDocument(cwd, entry, options: Record<string, any> = {}) {
|
|
96
|
+
const shouldCompute = options.computePath || !entry?.path;
|
|
97
|
+
const absPath = shouldCompute
|
|
98
|
+
? resolveTicketEntryOrComputedPath(cwd, entry)
|
|
99
|
+
: resolveTicketEntryPath(cwd, entry);
|
|
100
|
+
const fallbackAbsPath = shouldCompute ? absPath : resolveTicketEntryOrComputedPath(cwd, entry);
|
|
101
|
+
const finalAbsPath = absPath === fallbackAbsPath ? absPath : fallbackAbsPath;
|
|
102
|
+
|
|
103
|
+
const parse = options.parse ?? true;
|
|
104
|
+
const requireExists = options.requireExists ?? true;
|
|
105
|
+
const action = options.action || "ticket";
|
|
106
|
+
|
|
107
|
+
const exists = existsSync(finalAbsPath);
|
|
108
|
+
if (!exists && requireExists) {
|
|
109
|
+
const target = entry?.path || "unknown";
|
|
110
|
+
throw new Error(`${action}: Ticket file not found: ${target}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!exists) {
|
|
114
|
+
return {
|
|
115
|
+
absPath: finalAbsPath,
|
|
116
|
+
exists: false,
|
|
117
|
+
body: "",
|
|
118
|
+
meta: {},
|
|
119
|
+
content: "",
|
|
120
|
+
docmeta: null,
|
|
121
|
+
docmetaRaw: ""
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = readFileSync(finalAbsPath, "utf8");
|
|
126
|
+
if (!parse) {
|
|
127
|
+
return { absPath: finalAbsPath, exists: true, body, meta: {}, content: "", docmeta: null, docmetaRaw: "" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parsed = parseFrontMatter(body);
|
|
131
|
+
const ticketDocMeta = splitTicketDocMetaSection(parsed.content || "");
|
|
132
|
+
return {
|
|
133
|
+
absPath: finalAbsPath,
|
|
134
|
+
exists: true,
|
|
135
|
+
body,
|
|
136
|
+
...parsed,
|
|
137
|
+
content: ticketDocMeta.body,
|
|
138
|
+
docmeta: ticketDocMeta.docmeta,
|
|
139
|
+
docmetaRaw: ticketDocMeta.docmetaRaw
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function renderTicketDocument({ meta = {} as Record<string, any>, content = "", docmeta = null } = {}) {
|
|
144
|
+
return stringifyFrontMatter(meta, appendTicketDocMetaSection(content, docmeta));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function writeTicketMarkdownFile(absPath, body) {
|
|
148
|
+
writeFileLF(absPath, normalizeMarkdownContent(body));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writeTicketDocument(absPath, doc = {}) {
|
|
152
|
+
writeTicketMarkdownFile(absPath, renderTicketDocument(doc));
|
|
153
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { CliOpts, toSnakeCaseKey } from "./cli-utils.js";
|
|
2
|
+
|
|
3
|
+
export function hasPlaceholderTokens(text) {
|
|
4
|
+
const lines = String(text || "")
|
|
5
|
+
.split("\n")
|
|
6
|
+
.map(line => line.trim().toLowerCase())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
|
|
9
|
+
return lines.some(line => {
|
|
10
|
+
const normalized = line
|
|
11
|
+
.replace(/^[-*]\s+/, "")
|
|
12
|
+
.replace(/^\d+\.\s+/, "")
|
|
13
|
+
.trim();
|
|
14
|
+
return /^\[(?:add|fill)[^\]]*\]$/.test(normalized)
|
|
15
|
+
|| /^<[^>]*(?:placeholder|todo|tbd|fill|add)[^>]*>$/.test(normalized)
|
|
16
|
+
|| /^(?:placeholder|todo|tbd)$/.test(normalized);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ANALYSIS_DESIGN_SECTION_REQUIREMENTS = [
|
|
21
|
+
{
|
|
22
|
+
section: "Analysis",
|
|
23
|
+
reason: "analysis_missing",
|
|
24
|
+
scaffolds: [
|
|
25
|
+
"Record root-cause analysis, relevant file:line evidence, and hypotheses here."
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
section: "Direction",
|
|
30
|
+
reason: "direction_missing",
|
|
31
|
+
scaffolds: [
|
|
32
|
+
"Record the proposed fix direction or follow-up design path."
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const REQUIRED_PHASE1_SECTIONS = [
|
|
38
|
+
"Scope & Approval",
|
|
39
|
+
"Plan",
|
|
40
|
+
"Analysis",
|
|
41
|
+
"Direction"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const REQUIRED_APC_MARKERS = [];
|
|
45
|
+
|
|
46
|
+
const PHASE1_DATA_SECTIONS = [
|
|
47
|
+
"Plan",
|
|
48
|
+
"Analysis",
|
|
49
|
+
"Direction"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function dataSectionsForType(_type = "") {
|
|
53
|
+
return [...PHASE1_DATA_SECTIONS];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveTicketType(_opts) {
|
|
57
|
+
return "standard";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FOLLOW_UP_DECISION_NO_FOLLOW_UP_PATTERNS = [
|
|
61
|
+
/\bno[- ]follow[- ]up\b/i,
|
|
62
|
+
/\bfollow[- ]up\s*:\s*none\b/i,
|
|
63
|
+
/\bno further action\b/i,
|
|
64
|
+
/\bnone required\b/i,
|
|
65
|
+
/후속(?:\s+작업)?\s*(?:없음|불필요)/,
|
|
66
|
+
/추가(?:\s+작업)?\s*(?:없음|불필요)/
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function parseMarkdownH2Sections(content) {
|
|
70
|
+
const lines = String(content || "").split("\n");
|
|
71
|
+
const sections = new Map();
|
|
72
|
+
let currentHeading = "";
|
|
73
|
+
let buffer = [];
|
|
74
|
+
|
|
75
|
+
const flush = () => {
|
|
76
|
+
if (!currentHeading) return;
|
|
77
|
+
sections.set(currentHeading, buffer.join("\n").trim());
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const headingMatch = line.match(/^##\s+(.+?)\s*$/);
|
|
82
|
+
if (headingMatch) {
|
|
83
|
+
flush();
|
|
84
|
+
currentHeading = headingMatch[1].trim();
|
|
85
|
+
buffer = [];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (currentHeading) {
|
|
89
|
+
buffer.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
flush();
|
|
94
|
+
return sections;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const PHASE1_HEADING_ALIASES = new Map([
|
|
98
|
+
["apc", "Scope & Approval"],
|
|
99
|
+
["agent permission contract", "Scope & Approval"],
|
|
100
|
+
["agent permission contract (apc)", "Scope & Approval"],
|
|
101
|
+
["scope", "Scope & Approval"],
|
|
102
|
+
["compact plan", "Plan"],
|
|
103
|
+
["problem analysis", "Analysis"],
|
|
104
|
+
["source observations", "Analysis"],
|
|
105
|
+
["cause hypotheses", "Analysis"],
|
|
106
|
+
["audit evidence", "Analysis"],
|
|
107
|
+
["improvement direction", "Direction"],
|
|
108
|
+
...REQUIRED_PHASE1_SECTIONS.map(section => [section.toLowerCase(), section] as [string, string])
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
export function normalizePhase1PlanBodyHeadings(body) {
|
|
112
|
+
return String(body || "").split("\n").map((line) => {
|
|
113
|
+
const match = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
114
|
+
if (!match) return line;
|
|
115
|
+
const normalized = PHASE1_HEADING_ALIASES.get(match[1].trim().toLowerCase());
|
|
116
|
+
return normalized ? `## ${normalized}` : line;
|
|
117
|
+
}).join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function extractMarkdownSection(content, heading) {
|
|
121
|
+
return parseMarkdownH2Sections(content).get(String(heading || "").trim()) || "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractMarkdownSectionByAliases(content, aliases) {
|
|
125
|
+
const sections = parseMarkdownH2Sections(content);
|
|
126
|
+
for (const alias of aliases) {
|
|
127
|
+
const value = sections.get(String(alias || "").trim());
|
|
128
|
+
if (value !== undefined) return value;
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasSubstantiveSectionContent(text, scaffolds = []) {
|
|
134
|
+
const src = String(text || "").trim();
|
|
135
|
+
if (!src || hasPlaceholderTokens(src)) return false;
|
|
136
|
+
const normalized = src.replace(/\s+/g, " ");
|
|
137
|
+
return !scaffolds.some(phrase => normalized.includes(phrase));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasApcMarker(text, marker) {
|
|
141
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
142
|
+
const markerPattern = new RegExp(`^\\s*(?:#{1,6}\\s+)?${escapedMarker}(?:\\s|$)`);
|
|
143
|
+
return String(text || "").split("\n").some(line => markerPattern.test(line));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getApcContentWithoutMarkers(text) {
|
|
147
|
+
return String(text || "")
|
|
148
|
+
.split("\n")
|
|
149
|
+
.map(line => {
|
|
150
|
+
for (const { marker } of REQUIRED_APC_MARKERS) {
|
|
151
|
+
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
152
|
+
const markerPattern = new RegExp(`^\\s*(?:#{1,6}\\s+)?${escapedMarker}\\s*`);
|
|
153
|
+
if (markerPattern.test(line)) return line.replace(markerPattern, "");
|
|
154
|
+
}
|
|
155
|
+
return line;
|
|
156
|
+
})
|
|
157
|
+
.join("\n")
|
|
158
|
+
.trim();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getMissingApcFields(text) {
|
|
162
|
+
return REQUIRED_APC_MARKERS
|
|
163
|
+
.filter(({ marker }) => !hasApcMarker(text, marker))
|
|
164
|
+
.map(field => field.name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getPhase1PlanBodyReasons(body, opts: CliOpts = {}) {
|
|
168
|
+
const content = String(body || "");
|
|
169
|
+
const sections = parseMarkdownH2Sections(content);
|
|
170
|
+
const reasons = [];
|
|
171
|
+
|
|
172
|
+
for (const sectionName of ["Scope & Approval", ...dataSectionsForType()]) {
|
|
173
|
+
const sectionKey = toSnakeCaseKey(sectionName);
|
|
174
|
+
if (!sections.has(sectionName)) {
|
|
175
|
+
reasons.push(`${sectionKey}_missing`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!hasSubstantiveSectionContent(sections.get(sectionName), [])) {
|
|
179
|
+
reasons.push(`${sectionKey}_incomplete`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return reasons;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function phase1SectionsForType(_type = "") {
|
|
187
|
+
return ["Agent Permission Contract (APC)", ...dataSectionsForType()];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getAnalysisDesignIncompleteReasons(meta, content) {
|
|
191
|
+
const reasons = [];
|
|
192
|
+
for (const requirement of ANALYSIS_DESIGN_SECTION_REQUIREMENTS) {
|
|
193
|
+
if (!hasSubstantiveSectionContent(extractMarkdownSection(content, requirement.section), requirement.scaffolds)) {
|
|
194
|
+
reasons.push(requirement.reason);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return reasons;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function hasSubstantiveFollowUpDecision(content) {
|
|
202
|
+
return hasSubstantiveSectionContent(extractMarkdownSection(content, "Follow-up Decision"), []);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function followUpDecisionMeansNoFollowUp(content) {
|
|
206
|
+
const text = extractMarkdownSection(content, "Follow-up Decision");
|
|
207
|
+
if (!hasSubstantiveSectionContent(text, [])) return false;
|
|
208
|
+
return FOLLOW_UP_DECISION_NO_FOLLOW_UP_PATTERNS.some(pattern => pattern.test(text));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function hasTicketCompletionEvidence(content) {
|
|
212
|
+
return hasSubstantiveSectionContent(extractMarkdownSection(content, "Completion Report"), [])
|
|
213
|
+
|| hasSubstantiveSectionContent(extractMarkdownSection(content, "Verification Outcome"), [])
|
|
214
|
+
|| followUpDecisionMeansNoFollowUp(content);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const PHASE2_EXECUTION_SLOT_HEADINGS = [
|
|
218
|
+
"실행 슬롯",
|
|
219
|
+
"Execution Slots",
|
|
220
|
+
"Execution",
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const PHASE2_EXECUTION_SLOT_FIELDS = [
|
|
224
|
+
/files\s+changed\s*:/i,
|
|
225
|
+
/commands?\s+run\s*:/i,
|
|
226
|
+
/interim\s+evidence\s*:/i,
|
|
227
|
+
/approved\s+scope\s*:/i,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
export function hasPhase2ExecutionEvidence(content) {
|
|
231
|
+
const body = String(content || "");
|
|
232
|
+
// Check any phase2 execution slot heading section has non-empty content
|
|
233
|
+
for (const heading of PHASE2_EXECUTION_SLOT_HEADINGS) {
|
|
234
|
+
const section = extractMarkdownSection(body, heading);
|
|
235
|
+
if (section) {
|
|
236
|
+
const hasFilledField = PHASE2_EXECUTION_SLOT_FIELDS.some(pattern => {
|
|
237
|
+
const match = section.match(new RegExp(pattern.source + "\\s*(.+)", "i"));
|
|
238
|
+
return match && match[1] && match[1].trim().length > 0;
|
|
239
|
+
});
|
|
240
|
+
if (hasFilledField) return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fallback: any substantive content in Analysis or Direction (for bug-fix style tickets)
|
|
244
|
+
return hasSubstantiveSectionContent(extractMarkdownSection(body, "Analysis"), [])
|
|
245
|
+
|| hasSubstantiveSectionContent(extractMarkdownSection(body, "Problem Analysis"), []);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Compact, .md-aware slot status so `ticket use` shows what is already filled
|
|
249
|
+
// vs. still empty, plus affected files — preventing the agent from re-reading the
|
|
250
|
+
// ticket .md / find-grepping the tree to reconstruct context. Output stays terse.
|
|
251
|
+
const SLOT_STATUS_SECTIONS_BY_PHASE = {
|
|
252
|
+
1: ["Scope & Approval", "Plan", "Analysis", "Direction"],
|
|
253
|
+
2: ["Scope & Approval", "Plan", "Analysis", "Direction"],
|
|
254
|
+
3: ["Analysis", "Direction", "Execution Result", "Verification"],
|
|
255
|
+
4: ["Execution Result", "Verification", "Residual Risk", "Follow-up Decision"]
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Pull file-ish tokens (path/with/slashes or name.ext) from the sections most
|
|
259
|
+
// likely to record affected files, capped so the line never sprawls.
|
|
260
|
+
function extractAffectedFiles(content, limit = 6) {
|
|
261
|
+
const src = [
|
|
262
|
+
extractMarkdownSection(content, "Execution Result"),
|
|
263
|
+
extractMarkdownSection(content, "Scope & Approval"),
|
|
264
|
+
extractMarkdownSection(content, "Plan")
|
|
265
|
+
].join("\n");
|
|
266
|
+
const matches = src.match(/[\w./-]+\.[A-Za-z][\w]{0,4}\b/g) || [];
|
|
267
|
+
const seen = [];
|
|
268
|
+
for (const m of matches) {
|
|
269
|
+
const f = m.replace(/^[-*\s]+/, "");
|
|
270
|
+
if (!seen.includes(f)) seen.push(f);
|
|
271
|
+
if (seen.length >= limit) break;
|
|
272
|
+
}
|
|
273
|
+
return seen;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function buildSlotStatusSummary(content, phase = 2) {
|
|
277
|
+
const sections = SLOT_STATUS_SECTIONS_BY_PHASE[phase >= 4 ? 4 : phase] || SLOT_STATUS_SECTIONS_BY_PHASE[2];
|
|
278
|
+
const filled = [];
|
|
279
|
+
const empty = [];
|
|
280
|
+
for (const name of sections) {
|
|
281
|
+
if (hasSubstantiveSectionContent(extractMarkdownSection(content, name), [])) filled.push(name);
|
|
282
|
+
else empty.push(name);
|
|
283
|
+
}
|
|
284
|
+
const files = extractAffectedFiles(content);
|
|
285
|
+
const lines = [];
|
|
286
|
+
lines.push(`[filled] ${filled.length ? filled.join(" · ") : "(none)"}`);
|
|
287
|
+
lines.push(`[empty] ${empty.length ? empty.join(" · ") : "(none)"}`);
|
|
288
|
+
if (files.length) lines.push(`[files] ${files.join(", ")}`);
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getCloseWorkflowReasons(meta, content) {
|
|
293
|
+
const reasons = [];
|
|
294
|
+
const analysisScaffolds = ANALYSIS_DESIGN_SECTION_REQUIREMENTS[0].scaffolds;
|
|
295
|
+
const directionScaffolds = ANALYSIS_DESIGN_SECTION_REQUIREMENTS[1].scaffolds;
|
|
296
|
+
// Accept both canonical ("Analysis") and legacy ("Problem Analysis") heading names.
|
|
297
|
+
const analysisText = extractMarkdownSection(content, "Analysis")
|
|
298
|
+
|| extractMarkdownSection(content, "Problem Analysis");
|
|
299
|
+
if (!hasSubstantiveSectionContent(analysisText, analysisScaffolds)) {
|
|
300
|
+
reasons.push("problem_analysis_missing");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Accept both canonical ("Direction") and legacy ("Improvement Direction") heading names.
|
|
304
|
+
const directionText = extractMarkdownSection(content, "Direction")
|
|
305
|
+
|| extractMarkdownSection(content, "Improvement Direction");
|
|
306
|
+
if (!hasSubstantiveSectionContent(directionText, directionScaffolds)
|
|
307
|
+
&& !hasSubstantiveFollowUpDecision(content)) {
|
|
308
|
+
reasons.push("follow_up_decision_missing");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return reasons;
|
|
312
|
+
}
|