lee-spec-kit 0.8.3 → 0.8.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/README.en.md +2 -0
- package/README.md +2 -0
- package/dist/{hooks-B5UIIZYN.js → hooks-43P4YKHY.js} +3 -4
- package/dist/hooks-43P4YKHY.js.map +1 -0
- package/dist/index.js +420 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/en/common/README.md +1 -4
- package/templates/en/common/agents/agents.md +1 -0
- package/templates/en/common/agents/git-workflow.md +14 -2
- package/templates/en/common/agents/skills/execute-task.md +1 -1
- package/templates/en/common/features/README.md +4 -8
- package/templates/en/common/features/feature-base/decisions.md +1 -0
- package/templates/en/common/features/feature-base/tasks.md +4 -3
- package/templates/ko/common/README.md +1 -4
- package/templates/ko/common/agents/agents.md +1 -0
- package/templates/ko/common/agents/git-workflow.md +13 -2
- package/templates/ko/common/agents/skills/execute-task.md +1 -1
- package/templates/ko/common/features/README.md +4 -8
- package/templates/ko/common/features/feature-base/decisions.md +1 -0
- package/templates/ko/common/features/feature-base/tasks.md +4 -3
- package/dist/hooks-B5UIIZYN.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1297,8 +1297,9 @@ Before taking the next workflow step:
|
|
|
1297
1297
|
- task commit checkpoints after each completed task
|
|
1298
1298
|
8. In standalone mode, keep the docs repo on its docs branch and do not create feature branches or worktrees there
|
|
1299
1299
|
9. In standalone mode, use the project repo through its managed feature worktree under the shared workspace \`.worktrees/\` root instead of checking the feature branch out in the main project repo
|
|
1300
|
-
10.
|
|
1301
|
-
11.
|
|
1300
|
+
10. In standalone mode, do not hand-write \`git worktree add\`; run the exact \`nextAction.command\` from \`workflow-stage\` so the managed workspace path, stale directory cleanup, and \`.env\` / \`.env.*\` copy step stay consistent
|
|
1301
|
+
11. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
|
|
1302
|
+
12. When docs are synced to code, refresh an explicit marker like \`<!-- lee-spec-kit:workflow-sync 2026-04-16T12:34:56.789Z -->\` in the active feature docs (prefer \`tasks.md\` or \`decisions.md\`) so \`workflow-audit\` can prove the sync happened after the latest code change
|
|
1302
1303
|
|
|
1303
1304
|
Approval and remote actions:
|
|
1304
1305
|
|
|
@@ -1464,10 +1465,33 @@ function resolveManagedWorktreePath(config, projectRoot, branchName) {
|
|
|
1464
1465
|
normalizeBranchNameForWorktree(branchName)
|
|
1465
1466
|
);
|
|
1466
1467
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1468
|
+
var registeredWorktreeCache = /* @__PURE__ */ new Map();
|
|
1469
|
+
function listRegisteredGitWorktrees(projectRoot) {
|
|
1470
|
+
const resolvedProjectRoot = path8.resolve(projectRoot);
|
|
1471
|
+
const cached = registeredWorktreeCache.get(resolvedProjectRoot);
|
|
1472
|
+
if (cached) return cached;
|
|
1473
|
+
const output = runGitCapture(
|
|
1474
|
+
["worktree", "list", "--porcelain"],
|
|
1475
|
+
resolvedProjectRoot
|
|
1476
|
+
) || "";
|
|
1477
|
+
const worktrees = /* @__PURE__ */ new Set();
|
|
1478
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
1479
|
+
if (!line.startsWith("worktree ")) continue;
|
|
1480
|
+
const listedPath = line.slice("worktree ".length).trim();
|
|
1481
|
+
if (listedPath) worktrees.add(path8.resolve(listedPath));
|
|
1482
|
+
}
|
|
1483
|
+
registeredWorktreeCache.set(resolvedProjectRoot, worktrees);
|
|
1484
|
+
return worktrees;
|
|
1485
|
+
}
|
|
1486
|
+
function isRegisteredGitWorktree(projectRoot, worktreePath) {
|
|
1487
|
+
const resolvedTarget = path8.resolve(worktreePath);
|
|
1488
|
+
return listRegisteredGitWorktrees(projectRoot).has(resolvedTarget);
|
|
1489
|
+
}
|
|
1490
|
+
function buildManagedWorktreeStaleCleanupCommand(projectRoot, worktreePath) {
|
|
1491
|
+
return `if [ -d "${worktreePath}" ] && ! git -C "${projectRoot}" worktree list --porcelain | grep -Fxq "worktree ${worktreePath}"; then rm -rf "${worktreePath}"; fi`;
|
|
1492
|
+
}
|
|
1493
|
+
function buildManagedWorktreeEnvCopyCommand(projectRoot, worktreePath) {
|
|
1494
|
+
return `sh -c 'source_dir=$1; target_dir=$2; for source_env in "$source_dir"/.env "$source_dir"/.env.*; do [ -e "$source_env" ] || [ -L "$source_env" ] || continue; target_env="$target_dir/$(basename "$source_env")"; if [ ! -e "$target_env" ] && [ ! -L "$target_env" ]; then cp -p "$source_env" "$target_env"; fi; done' sh "${path8.resolve(projectRoot)}" "${path8.resolve(worktreePath)}"`;
|
|
1471
1495
|
}
|
|
1472
1496
|
|
|
1473
1497
|
// src/utils/init/options.ts
|
|
@@ -4023,7 +4047,7 @@ async function resolveExistingManagedWorktreePath(config, projectGitCwd, slug, f
|
|
|
4023
4047
|
(candidate) => resolveManagedWorktreePath(config, projectRoot, candidate)
|
|
4024
4048
|
);
|
|
4025
4049
|
for (const candidate of candidates) {
|
|
4026
|
-
if (await fs.pathExists(candidate)) {
|
|
4050
|
+
if (await fs.pathExists(candidate) && isRegisteredGitWorktree(projectRoot, candidate)) {
|
|
4027
4051
|
return candidate;
|
|
4028
4052
|
}
|
|
4029
4053
|
}
|
|
@@ -6744,6 +6768,380 @@ function docsCommand(program2) {
|
|
|
6744
6768
|
}
|
|
6745
6769
|
});
|
|
6746
6770
|
}
|
|
6771
|
+
|
|
6772
|
+
// src/utils/task-lines.ts
|
|
6773
|
+
function parseTaskLine(line, index = -1) {
|
|
6774
|
+
const match = line.match(
|
|
6775
|
+
/^\s*-\s*\[(TODO|DOING|DONE|REVIEW)\]((?:\[[^\]]+\])*)\s+(T-[A-Za-z0-9-]+)\s+(.+?)\s*$/
|
|
6776
|
+
);
|
|
6777
|
+
if (!match) return null;
|
|
6778
|
+
const tags = [...(match[2] || "").matchAll(/\[([^\]]+)\]/g)].map((entry) => (entry[1] || "").trim()).filter(Boolean);
|
|
6779
|
+
return {
|
|
6780
|
+
index,
|
|
6781
|
+
raw: line,
|
|
6782
|
+
status: match[1],
|
|
6783
|
+
tags,
|
|
6784
|
+
taskId: match[3],
|
|
6785
|
+
title: match[4]
|
|
6786
|
+
};
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
// src/utils/doc-mutation.ts
|
|
6790
|
+
function collectRepeatableOption(value, previous = []) {
|
|
6791
|
+
return [...previous, value];
|
|
6792
|
+
}
|
|
6793
|
+
function normalizeRequiredText(value, label) {
|
|
6794
|
+
const normalized = (value || "").trim();
|
|
6795
|
+
if (!normalized || normalized === "-" || /^todo$/i.test(normalized)) {
|
|
6796
|
+
throw createCliError(
|
|
6797
|
+
"INVALID_ARGUMENT",
|
|
6798
|
+
`${label} must contain concrete text.`
|
|
6799
|
+
);
|
|
6800
|
+
}
|
|
6801
|
+
return normalized;
|
|
6802
|
+
}
|
|
6803
|
+
function normalizeRequiredItems(values, label) {
|
|
6804
|
+
const normalized = (values || []).map((value) => value.trim()).filter(Boolean);
|
|
6805
|
+
if (normalized.length === 0) {
|
|
6806
|
+
throw createCliError(
|
|
6807
|
+
"INVALID_ARGUMENT",
|
|
6808
|
+
`${label} must be provided at least once.`
|
|
6809
|
+
);
|
|
6810
|
+
}
|
|
6811
|
+
for (const value of normalized) {
|
|
6812
|
+
normalizeRequiredText(value, label);
|
|
6813
|
+
}
|
|
6814
|
+
return normalized;
|
|
6815
|
+
}
|
|
6816
|
+
async function resolveFeatureDocTarget(input) {
|
|
6817
|
+
const state = await resolveFeatureSelection(
|
|
6818
|
+
input.cwd,
|
|
6819
|
+
input.selector,
|
|
6820
|
+
input.component
|
|
6821
|
+
);
|
|
6822
|
+
if (state.status !== "selected" || !state.matchedFeature) {
|
|
6823
|
+
throw createCliError(
|
|
6824
|
+
"CONTEXT_SELECTION_REQUIRED",
|
|
6825
|
+
`A single feature is required. Pass <feature-name> explicitly.`
|
|
6826
|
+
);
|
|
6827
|
+
}
|
|
6828
|
+
const targetPath = path8.join(state.matchedFeature.path, input.fileName);
|
|
6829
|
+
if (!await fs.pathExists(targetPath)) {
|
|
6830
|
+
throw createCliError(
|
|
6831
|
+
"PRECONDITION_FAILED",
|
|
6832
|
+
`${input.fileName} not found for feature: ${state.matchedFeature.folderName}`
|
|
6833
|
+
);
|
|
6834
|
+
}
|
|
6835
|
+
return {
|
|
6836
|
+
feature: state.matchedFeature,
|
|
6837
|
+
path: targetPath
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6840
|
+
function findSecondLevelHeadingIndex(lines, names) {
|
|
6841
|
+
const alternatives = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
6842
|
+
const pattern = new RegExp(`^\\s*##\\s+(${alternatives.join("|")})\\s*$`);
|
|
6843
|
+
return lines.findIndex((line) => pattern.test(line));
|
|
6844
|
+
}
|
|
6845
|
+
function findNextSecondLevelHeadingIndex(lines, start) {
|
|
6846
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
6847
|
+
if (/^\s*##\s+/.test(lines[index] || "")) return index;
|
|
6848
|
+
}
|
|
6849
|
+
return lines.length;
|
|
6850
|
+
}
|
|
6851
|
+
function normalizeMarkdownEnd(content) {
|
|
6852
|
+
return content.replace(/\s+$/g, "") + "\n";
|
|
6853
|
+
}
|
|
6854
|
+
function localDate() {
|
|
6855
|
+
return getLocalDateString();
|
|
6856
|
+
}
|
|
6857
|
+
function nextTaskSequence(content, featureFolderName) {
|
|
6858
|
+
const escaped = featureFolderName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6859
|
+
const taskIdPattern = new RegExp(`\\bT-${escaped}-(\\d+)\\b`, "g");
|
|
6860
|
+
let max = 0;
|
|
6861
|
+
for (const match of content.matchAll(taskIdPattern)) {
|
|
6862
|
+
const parsed = Number(match[1] || "0");
|
|
6863
|
+
if (Number.isFinite(parsed) && parsed > max) max = parsed;
|
|
6864
|
+
}
|
|
6865
|
+
return max + 1;
|
|
6866
|
+
}
|
|
6867
|
+
function findTaskInsertIndex(lines, sectionStart, sectionEnd) {
|
|
6868
|
+
let lastTaskIndex = -1;
|
|
6869
|
+
for (let index = sectionStart; index < sectionEnd; index += 1) {
|
|
6870
|
+
if (parseTaskLine(lines[index] || "", index)) lastTaskIndex = index;
|
|
6871
|
+
}
|
|
6872
|
+
if (lastTaskIndex < 0) return sectionEnd;
|
|
6873
|
+
let insertIndex = lastTaskIndex + 1;
|
|
6874
|
+
while (insertIndex < sectionEnd) {
|
|
6875
|
+
const line = lines[insertIndex] || "";
|
|
6876
|
+
if (parseTaskLine(line, insertIndex)) break;
|
|
6877
|
+
if (/^\s{2,}\S/.test(line) || /^\s*$/.test(line)) {
|
|
6878
|
+
insertIndex += 1;
|
|
6879
|
+
continue;
|
|
6880
|
+
}
|
|
6881
|
+
break;
|
|
6882
|
+
}
|
|
6883
|
+
return insertIndex;
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
// src/commands/task.ts
|
|
6887
|
+
function normalizeTaskRef(value) {
|
|
6888
|
+
const ref = normalizeRequiredText(value, "--ref").toUpperCase();
|
|
6889
|
+
if (ref === "NON-PRD") return ref;
|
|
6890
|
+
if (!/^PRD-[A-Z0-9][A-Z0-9-]*$/.test(ref)) {
|
|
6891
|
+
throw createCliError(
|
|
6892
|
+
"INVALID_ARGUMENT",
|
|
6893
|
+
"`--ref` must be `NON-PRD` or a `PRD-*` requirement key."
|
|
6894
|
+
);
|
|
6895
|
+
}
|
|
6896
|
+
return ref;
|
|
6897
|
+
}
|
|
6898
|
+
function formatTaskBlock(input) {
|
|
6899
|
+
return [
|
|
6900
|
+
`- [TODO][${input.ref}] ${input.taskId} ${input.title}`,
|
|
6901
|
+
` - Date: ${input.date}`,
|
|
6902
|
+
" - Acceptance:",
|
|
6903
|
+
...input.acceptanceItems.map((item) => ` - ${item}`),
|
|
6904
|
+
" - Checklist:",
|
|
6905
|
+
...input.checklistItems.map((item) => ` - [ ] ${item}`)
|
|
6906
|
+
];
|
|
6907
|
+
}
|
|
6908
|
+
async function runTaskAdd(featureName, options) {
|
|
6909
|
+
const target = await resolveFeatureDocTarget({
|
|
6910
|
+
cwd: process.cwd(),
|
|
6911
|
+
selector: featureName,
|
|
6912
|
+
component: options.component,
|
|
6913
|
+
fileName: "tasks.md"
|
|
6914
|
+
});
|
|
6915
|
+
const title = normalizeRequiredText(options.title, "--title");
|
|
6916
|
+
const ref = normalizeTaskRef(options.ref);
|
|
6917
|
+
const acceptanceItems = normalizeRequiredItems(options.acceptance, "--acceptance");
|
|
6918
|
+
const checklistItems = normalizeRequiredItems(options.check, "--check");
|
|
6919
|
+
const content = await fs.readFile(target.path, "utf-8");
|
|
6920
|
+
const lines = content.split("\n");
|
|
6921
|
+
const taskListIndex = findSecondLevelHeadingIndex(lines, ["Task List", "\uD0DC\uC2A4\uD06C \uBAA9\uB85D"]);
|
|
6922
|
+
if (taskListIndex < 0) {
|
|
6923
|
+
throw createCliError(
|
|
6924
|
+
"PRECONDITION_FAILED",
|
|
6925
|
+
"tasks.md is missing a `Task List` section."
|
|
6926
|
+
);
|
|
6927
|
+
}
|
|
6928
|
+
const sectionEnd = findNextSecondLevelHeadingIndex(lines, taskListIndex);
|
|
6929
|
+
const insertIndex = findTaskInsertIndex(lines, taskListIndex + 1, sectionEnd);
|
|
6930
|
+
const taskId = `T-${target.feature.folderName}-${String(
|
|
6931
|
+
nextTaskSequence(content, target.feature.folderName)
|
|
6932
|
+
).padStart(2, "0")}`;
|
|
6933
|
+
const recordedAt = localDate();
|
|
6934
|
+
const block = formatTaskBlock({
|
|
6935
|
+
ref,
|
|
6936
|
+
taskId,
|
|
6937
|
+
title,
|
|
6938
|
+
date: recordedAt,
|
|
6939
|
+
acceptanceItems,
|
|
6940
|
+
checklistItems
|
|
6941
|
+
});
|
|
6942
|
+
const shouldPrefixBlank = insertIndex > taskListIndex + 1 && (lines[insertIndex - 1] || "").trim() !== "";
|
|
6943
|
+
const shouldSuffixBlank = insertIndex < lines.length && (lines[insertIndex] || "").trim() !== "";
|
|
6944
|
+
lines.splice(
|
|
6945
|
+
insertIndex,
|
|
6946
|
+
0,
|
|
6947
|
+
...shouldPrefixBlank ? [""] : [],
|
|
6948
|
+
...block,
|
|
6949
|
+
...shouldSuffixBlank ? [""] : []
|
|
6950
|
+
);
|
|
6951
|
+
await fs.writeFile(target.path, normalizeMarkdownEnd(lines.join("\n")), "utf-8");
|
|
6952
|
+
const payload = {
|
|
6953
|
+
status: "ok",
|
|
6954
|
+
reasonCode: "TASK_ADDED",
|
|
6955
|
+
feature: target.feature.folderName,
|
|
6956
|
+
taskId,
|
|
6957
|
+
title,
|
|
6958
|
+
ref,
|
|
6959
|
+
tasksUpdated: true,
|
|
6960
|
+
tasksPath: target.path,
|
|
6961
|
+
recordedAt
|
|
6962
|
+
};
|
|
6963
|
+
if (options.json) {
|
|
6964
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
6965
|
+
return;
|
|
6966
|
+
}
|
|
6967
|
+
console.log(chalk.green(`Added task ${taskId} to ${target.feature.folderName}.`));
|
|
6968
|
+
console.log(chalk.gray(`- tasks.md updated: ${target.path}`));
|
|
6969
|
+
}
|
|
6970
|
+
function taskCommand(program2) {
|
|
6971
|
+
const task = program2.command("task").description("Patch feature task docs");
|
|
6972
|
+
task.command("add [feature-name]").description("Append a docs-only task block to tasks.md").requiredOption("--title <title>", "Task title").requiredOption("--ref <ref>", "Requirement ref: NON-PRD or PRD-* key").option(
|
|
6973
|
+
"--acceptance <text>",
|
|
6974
|
+
"Concrete acceptance item. Repeat to add more than one.",
|
|
6975
|
+
collectRepeatableOption,
|
|
6976
|
+
[]
|
|
6977
|
+
).option(
|
|
6978
|
+
"--check <text>",
|
|
6979
|
+
"Concrete checklist item. Repeat to add more than one.",
|
|
6980
|
+
collectRepeatableOption,
|
|
6981
|
+
[]
|
|
6982
|
+
).option("--component <component>", "Component name for multi projects").option("--json", "Output JSON").action(async (featureName, options) => {
|
|
6983
|
+
try {
|
|
6984
|
+
await runTaskAdd(featureName, options);
|
|
6985
|
+
} catch (error) {
|
|
6986
|
+
const cliError = toCliError(error);
|
|
6987
|
+
const suggestions = getCliErrorSuggestions(cliError.code, DEFAULT_LANG);
|
|
6988
|
+
if (options.json) {
|
|
6989
|
+
console.log(
|
|
6990
|
+
JSON.stringify({
|
|
6991
|
+
status: "error",
|
|
6992
|
+
reasonCode: cliError.code,
|
|
6993
|
+
error: cliError.message,
|
|
6994
|
+
suggestions
|
|
6995
|
+
})
|
|
6996
|
+
);
|
|
6997
|
+
process.exitCode = 1;
|
|
6998
|
+
return;
|
|
6999
|
+
}
|
|
7000
|
+
console.error(chalk.red(`[${cliError.code}] ${cliError.message}`));
|
|
7001
|
+
printCliErrorSuggestions(suggestions, DEFAULT_LANG);
|
|
7002
|
+
process.exitCode = 1;
|
|
7003
|
+
}
|
|
7004
|
+
});
|
|
7005
|
+
}
|
|
7006
|
+
function getNextDecisionSequence(content) {
|
|
7007
|
+
let max = 0;
|
|
7008
|
+
for (const match of content.matchAll(/^##\s+D(\d+):\s+/gm)) {
|
|
7009
|
+
if (/\{(?:Decision Title|결정 제목)\}/.test(match[0])) continue;
|
|
7010
|
+
const parsed = Number(match[1] || "0");
|
|
7011
|
+
if (Number.isFinite(parsed) && parsed > max) max = parsed;
|
|
7012
|
+
}
|
|
7013
|
+
return max + 1;
|
|
7014
|
+
}
|
|
7015
|
+
function findPlaceholderDecisionRange(content) {
|
|
7016
|
+
const match = /^##\s+D(\d+):\s+.*$/m.exec(content);
|
|
7017
|
+
if (!match || match.index === void 0) return null;
|
|
7018
|
+
const afterHeadingIndex = match.index + match[0].length;
|
|
7019
|
+
const nextHeadingMatch = /^##\s+D\d+:\s+/m.exec(content.slice(afterHeadingIndex));
|
|
7020
|
+
const end = nextHeadingMatch ? afterHeadingIndex + nextHeadingMatch.index : content.length;
|
|
7021
|
+
const block = content.slice(match.index, end);
|
|
7022
|
+
const isPlaceholder = /\{(?:Decision Title|결정 제목)\}/.test(match[0]) || /-\s+\*\*Decision\*\*:\s*(Final choice|최종 선택)/.test(block);
|
|
7023
|
+
if (!isPlaceholder) return null;
|
|
7024
|
+
const sequence = Number(match[1] || "1");
|
|
7025
|
+
return {
|
|
7026
|
+
start: match.index,
|
|
7027
|
+
end,
|
|
7028
|
+
sequence: Number.isFinite(sequence) ? sequence : 1
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
function formatOptions(options) {
|
|
7032
|
+
return options.length > 0 ? options.join("; ") : "-";
|
|
7033
|
+
}
|
|
7034
|
+
function formatDecisionBlock(input) {
|
|
7035
|
+
return [
|
|
7036
|
+
`## ${input.decisionId}: ${input.title} (${input.date})`,
|
|
7037
|
+
"",
|
|
7038
|
+
`- **Context**: ${input.context}`,
|
|
7039
|
+
`- **Constraints**: ${input.constraints}`,
|
|
7040
|
+
`- **Options**: ${formatOptions(input.options)}`,
|
|
7041
|
+
`- **Decision**: ${input.decision}`,
|
|
7042
|
+
`- **Rationale**: ${input.rationale}`,
|
|
7043
|
+
"- **Trace**:",
|
|
7044
|
+
" - **At DOING start**: Recorded by `decision add` when the decision was created.",
|
|
7045
|
+
" - **Before DONE**: Update this line when the related task is completed.",
|
|
7046
|
+
" - **Post-merge check**: Update this line after merge when applicable.",
|
|
7047
|
+
"- **Evidence**:",
|
|
7048
|
+
...input.evidence.map((item) => ` - **Test/Log**: ${item}`),
|
|
7049
|
+
`- **Consequences**: ${input.consequence}`
|
|
7050
|
+
].join("\n");
|
|
7051
|
+
}
|
|
7052
|
+
async function runDecisionAdd(featureName, options) {
|
|
7053
|
+
const target = await resolveFeatureDocTarget({
|
|
7054
|
+
cwd: process.cwd(),
|
|
7055
|
+
selector: featureName,
|
|
7056
|
+
component: options.component,
|
|
7057
|
+
fileName: "decisions.md"
|
|
7058
|
+
});
|
|
7059
|
+
const title = normalizeRequiredText(options.title, "--title");
|
|
7060
|
+
const context = normalizeRequiredText(options.context, "--context");
|
|
7061
|
+
const decision = normalizeRequiredText(options.decision, "--decision");
|
|
7062
|
+
const rationale = normalizeRequiredText(options.rationale, "--rationale");
|
|
7063
|
+
const evidence = normalizeRequiredItems(options.evidence, "--evidence");
|
|
7064
|
+
const constraints = (options.constraints || "").trim() || "-";
|
|
7065
|
+
const consequence = (options.consequence || "").trim() || "-";
|
|
7066
|
+
const optionItems = (options.option || []).map((value) => value.trim()).filter(Boolean);
|
|
7067
|
+
const content = await fs.readFile(target.path, "utf-8");
|
|
7068
|
+
const placeholderRange = findPlaceholderDecisionRange(content);
|
|
7069
|
+
const decisionSequence = placeholderRange?.sequence ?? getNextDecisionSequence(content);
|
|
7070
|
+
const decisionId = `D${String(decisionSequence).padStart(3, "0")}`;
|
|
7071
|
+
const recordedAt = localDate();
|
|
7072
|
+
const block = formatDecisionBlock({
|
|
7073
|
+
decisionId,
|
|
7074
|
+
title,
|
|
7075
|
+
date: recordedAt,
|
|
7076
|
+
context,
|
|
7077
|
+
constraints,
|
|
7078
|
+
options: optionItems,
|
|
7079
|
+
decision,
|
|
7080
|
+
rationale,
|
|
7081
|
+
evidence,
|
|
7082
|
+
consequence
|
|
7083
|
+
});
|
|
7084
|
+
const nextContent = placeholderRange ? normalizeMarkdownEnd(
|
|
7085
|
+
`${content.slice(0, placeholderRange.start)}${block}${content.slice(
|
|
7086
|
+
placeholderRange.end
|
|
7087
|
+
)}`
|
|
7088
|
+
) : `${normalizeMarkdownEnd(content)}
|
|
7089
|
+
${block}
|
|
7090
|
+
`;
|
|
7091
|
+
await fs.writeFile(target.path, nextContent, "utf-8");
|
|
7092
|
+
const payload = {
|
|
7093
|
+
status: "ok",
|
|
7094
|
+
reasonCode: "DECISION_ADDED",
|
|
7095
|
+
feature: target.feature.folderName,
|
|
7096
|
+
decisionId,
|
|
7097
|
+
title,
|
|
7098
|
+
decisionsUpdated: true,
|
|
7099
|
+
decisionsPath: target.path,
|
|
7100
|
+
recordedAt
|
|
7101
|
+
};
|
|
7102
|
+
if (options.json) {
|
|
7103
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7104
|
+
return;
|
|
7105
|
+
}
|
|
7106
|
+
console.log(chalk.green(`Added decision ${decisionId} to ${target.feature.folderName}.`));
|
|
7107
|
+
console.log(chalk.gray(`- decisions.md updated: ${target.path}`));
|
|
7108
|
+
}
|
|
7109
|
+
function decisionCommand(program2) {
|
|
7110
|
+
const decision = program2.command("decision").description("Patch feature decision docs");
|
|
7111
|
+
decision.command("add [feature-name]").description("Append a docs-only ADR block to decisions.md").requiredOption("--title <title>", "Decision title").requiredOption("--context <text>", "Decision context").option("--constraints <text>", "Decision constraints").option(
|
|
7112
|
+
"--option <text>",
|
|
7113
|
+
"Alternative considered. Repeat to add more than one.",
|
|
7114
|
+
collectRepeatableOption,
|
|
7115
|
+
[]
|
|
7116
|
+
).requiredOption("--decision <text>", "Final decision").requiredOption("--rationale <text>", "Decision rationale").option(
|
|
7117
|
+
"--evidence <text>",
|
|
7118
|
+
"Evidence link or test/log note. Repeat to add more than one.",
|
|
7119
|
+
collectRepeatableOption,
|
|
7120
|
+
[]
|
|
7121
|
+
).option("--consequence <text>", "Decision consequence").option("--component <component>", "Component name for multi projects").option("--json", "Output JSON").action(async (featureName, options) => {
|
|
7122
|
+
try {
|
|
7123
|
+
await runDecisionAdd(featureName, options);
|
|
7124
|
+
} catch (error) {
|
|
7125
|
+
const cliError = toCliError(error);
|
|
7126
|
+
const suggestions = getCliErrorSuggestions(cliError.code, DEFAULT_LANG);
|
|
7127
|
+
if (options.json) {
|
|
7128
|
+
console.log(
|
|
7129
|
+
JSON.stringify({
|
|
7130
|
+
status: "error",
|
|
7131
|
+
reasonCode: cliError.code,
|
|
7132
|
+
error: cliError.message,
|
|
7133
|
+
suggestions
|
|
7134
|
+
})
|
|
7135
|
+
);
|
|
7136
|
+
process.exitCode = 1;
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
console.error(chalk.red(`[${cliError.code}] ${cliError.message}`));
|
|
7140
|
+
printCliErrorSuggestions(suggestions, DEFAULT_LANG);
|
|
7141
|
+
process.exitCode = 1;
|
|
7142
|
+
}
|
|
7143
|
+
});
|
|
7144
|
+
}
|
|
6747
7145
|
function detectCommand(program2) {
|
|
6748
7146
|
program2.command("detect").description(tr(DEFAULT_LANG, "cli", "detect.cmdDescription")).option("--dir <dir>", tr(DEFAULT_LANG, "cli", "detect.optDir")).option("--json", tr(DEFAULT_LANG, "cli", "detect.optJson")).action(async (options) => {
|
|
6749
7147
|
try {
|
|
@@ -6924,7 +7322,7 @@ function registerCodexHooksIntegration(parent) {
|
|
|
6924
7322
|
removeLeeSpecKitCodexHooks,
|
|
6925
7323
|
resolveCodexHooksRepoRoot,
|
|
6926
7324
|
upsertLeeSpecKitCodexHooks
|
|
6927
|
-
} = await import('./hooks-
|
|
7325
|
+
} = await import('./hooks-43P4YKHY.js');
|
|
6928
7326
|
const repoRoot = config.docsRepo === "standalone" ? resolveConfiguredStandaloneWorkspaceRoot(config) : resolveCodexHooksRepoRoot(process.cwd());
|
|
6929
7327
|
if (!repoRoot) {
|
|
6930
7328
|
throw createCliError(
|
|
@@ -7385,15 +7783,20 @@ function getExpectedWorktreePath(config, projectGitCwd, branchName) {
|
|
|
7385
7783
|
return resolveManagedWorktreePath(config, projectRoot, branchName);
|
|
7386
7784
|
}
|
|
7387
7785
|
async function resolveExistingExpectedWorktreePath(config, projectGitCwd, branchName) {
|
|
7786
|
+
const projectRoot = resolveProjectRootFromGitCwd2(projectGitCwd);
|
|
7388
7787
|
const candidate = getExpectedWorktreePath(config, projectGitCwd, branchName);
|
|
7389
|
-
return await fs.pathExists(candidate) ? candidate : null;
|
|
7788
|
+
return await fs.pathExists(candidate) && isRegisteredGitWorktree(projectRoot, candidate) ? candidate : null;
|
|
7390
7789
|
}
|
|
7391
7790
|
function buildManagedWorktreeCreateCommand(config, projectGitCwd, branchName) {
|
|
7392
7791
|
const projectRoot = resolveProjectRootFromGitCwd2(projectGitCwd);
|
|
7393
7792
|
const worktreePath = getExpectedWorktreePath(config, projectGitCwd, branchName);
|
|
7394
7793
|
const worktreeParent = path8.dirname(worktreePath);
|
|
7395
|
-
const
|
|
7396
|
-
|
|
7794
|
+
const staleCleanupCommand = buildManagedWorktreeStaleCleanupCommand(
|
|
7795
|
+
projectRoot,
|
|
7796
|
+
worktreePath
|
|
7797
|
+
);
|
|
7798
|
+
const envCopyCommand = buildManagedWorktreeEnvCopyCommand(projectRoot, worktreePath);
|
|
7799
|
+
return `${staleCleanupCommand} && mkdir -p "${worktreeParent}" && (git -C "${projectRoot}" worktree add "${worktreePath}" "${branchName}" || git -C "${projectRoot}" worktree add -b "${branchName}" "${worktreePath}") && ${envCopyCommand}`;
|
|
7397
7800
|
}
|
|
7398
7801
|
function resolveRemotePrMergeMeta(prRef, projectGitCwd) {
|
|
7399
7802
|
if (!prRef) return null;
|
|
@@ -9224,10 +9627,10 @@ function parseStagedPaths(output) {
|
|
|
9224
9627
|
staged.set(`path:${normalizeSlashes3(parts[1])}`, `${status}:path`);
|
|
9225
9628
|
}
|
|
9226
9629
|
return [...staged.entries()].map(([encodedPath, encodedStatus]) => {
|
|
9227
|
-
const [role,
|
|
9630
|
+
const [role, path27] = encodedPath.split(":", 2);
|
|
9228
9631
|
const [status, entryRole] = encodedStatus.split(":", 2);
|
|
9229
9632
|
return {
|
|
9230
|
-
path:
|
|
9633
|
+
path: path27,
|
|
9231
9634
|
status,
|
|
9232
9635
|
role: entryRole || role || "path"
|
|
9233
9636
|
};
|
|
@@ -9502,6 +9905,8 @@ function configureRootCommandSurface() {
|
|
|
9502
9905
|
["init", "Docs Schema Commands:"],
|
|
9503
9906
|
["idea", "Docs Schema Commands:"],
|
|
9504
9907
|
["feature", "Docs Schema Commands:"],
|
|
9908
|
+
["task", "Docs Schema Commands:"],
|
|
9909
|
+
["decision", "Docs Schema Commands:"],
|
|
9505
9910
|
["docs", "Workflow Policy Commands:"],
|
|
9506
9911
|
["detect", "Workflow Policy Commands:"],
|
|
9507
9912
|
["github", "Workflow Policy Commands:"],
|
|
@@ -9531,6 +9936,8 @@ updateCommand(program);
|
|
|
9531
9936
|
configCommand(program);
|
|
9532
9937
|
githubCommand(program);
|
|
9533
9938
|
docsCommand(program);
|
|
9939
|
+
taskCommand(program);
|
|
9940
|
+
decisionCommand(program);
|
|
9534
9941
|
detectCommand(program);
|
|
9535
9942
|
workflowStageCommand(program);
|
|
9536
9943
|
integrationsCommand(program);
|