lee-spec-kit 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
1301
- 11. 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
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, keep exactly one explicit marker like \`<!-- lee-spec-kit:workflow-sync 2026-04-16T12:34:56.789Z -->\` in a single active feature doc (prefer \`tasks.md\` or \`decisions.md\`): replace an existing marker timestamp or remove duplicates instead of appending another marker, so \`workflow-audit\` can prove the sync happened after the latest code change
1302
1303
 
1303
1304
  Approval and remote actions:
1304
1305
 
@@ -1313,7 +1314,7 @@ Approval and remote actions:
1313
1314
  Validation:
1314
1315
 
1315
1316
  - Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation
1316
- - Prefer \`npx lee-spec-kit workflow-audit --json\` as the default docs-sync validator for Codex hooks and end-of-turn checks; it expects the active feature docs to carry a fresh \`lee-spec-kit:workflow-sync\` marker after meaningful code/doc sync
1317
+ - Prefer \`npx lee-spec-kit workflow-audit --json\` as the default docs-sync validator for Codex hooks and end-of-turn checks; it expects the active feature docs to carry one fresh \`lee-spec-kit:workflow-sync\` marker after meaningful code/doc sync
1317
1318
  `;
1318
1319
  function renderManagedSegment(lang, docsRepo) {
1319
1320
  return `${LEE_SPEC_KIT_AGENTS_BEGIN}
@@ -1464,10 +1465,33 @@ function resolveManagedWorktreePath(config, projectRoot, branchName) {
1464
1465
  normalizeBranchNameForWorktree(branchName)
1465
1466
  );
1466
1467
  }
1467
- function buildManagedWorktreeEnvLinkCommand(projectRoot, worktreePath) {
1468
- const sourceEnvPath = path8.resolve(projectRoot, ".env");
1469
- const targetEnvPath = path8.resolve(worktreePath, ".env");
1470
- return `if [ -f "${sourceEnvPath}" ] && [ ! -e "${targetEnvPath}" ] && [ ! -L "${targetEnvPath}" ]; then ln -s "${sourceEnvPath}" "${targetEnvPath}"; fi`;
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-B5UIIZYN.js');
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 envLinkCommand = buildManagedWorktreeEnvLinkCommand(projectRoot, worktreePath);
7396
- return `mkdir -p "${worktreeParent}" && (git -C "${projectRoot}" worktree add "${worktreePath}" "${branchName}" || git -C "${projectRoot}" worktree add -b "${branchName}" "${worktreePath}") && ${envLinkCommand}`;
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;
@@ -7484,7 +7887,7 @@ function buildPostMergeCleanupCommand(state) {
7484
7887
  }
7485
7888
  if (state.worktreePath) {
7486
7889
  commandParts.push(
7487
- `if [ -d "${state.worktreePath}" ]; then git -C "${state.projectRootGitCwd}" worktree remove "${state.worktreePath}"; fi`
7890
+ `if [ -d "${state.worktreePath}" ]; then if git -C "${state.worktreePath}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then meaningful_changes=$(git -C "${state.worktreePath}" status --porcelain --untracked-files=normal 2>/dev/null || true); if [ -n "$meaningful_changes" ]; then printf '%s\\n' "Managed worktree has tracked or meaningful untracked changes; refusing cleanup: ${state.worktreePath}" >&2; exit 1; fi; git -C "${state.projectRootGitCwd}" worktree remove --force "${state.worktreePath}" || { git -C "${state.projectRootGitCwd}" worktree prune; rm -rf "${state.worktreePath}"; }; else leftover_meaningful=$(find "${state.worktreePath}" -mindepth 1 \\( -name ".next" -o -name "node_modules" -o -name "storybook-static" -o -name "dist" -o -name "build" -o -name "coverage" -o -name ".turbo" -o -name ".cache" \\) -prune -o -print -quit); if [ -n "$leftover_meaningful" ]; then printf '%s\\n' "Managed worktree leftover has files outside generated artifact directories; refusing cleanup: ${state.worktreePath}" >&2; exit 1; fi; git -C "${state.projectRootGitCwd}" worktree prune; rm -rf "${state.worktreePath}"; fi; fi`
7488
7891
  );
7489
7892
  }
7490
7893
  if (state.headBranch) {
@@ -7493,7 +7896,7 @@ function buildPostMergeCleanupCommand(state) {
7493
7896
  );
7494
7897
  if (state.hasOriginRemote) {
7495
7898
  commandParts.push(
7496
- `if git -C "${state.projectRootGitCwd}" show-ref --verify --quiet "refs/remotes/origin/${state.headBranch}"; then git -C "${state.projectRootGitCwd}" push origin --delete "${state.headBranch}"; fi`
7899
+ `if git -C "${state.projectRootGitCwd}" show-ref --verify --quiet "refs/remotes/origin/${state.headBranch}"; then HUSKY=0 git -C "${state.projectRootGitCwd}" push origin --delete "${state.headBranch}"; fi`
7497
7900
  );
7498
7901
  commandParts.push(
7499
7902
  `git -C "${state.projectRootGitCwd}" fetch --prune origin`
@@ -7697,14 +8100,15 @@ function resolveCurrentReviewState(tasks, prDraft, remoteReviewState) {
7697
8100
  }
7698
8101
  return "unknown";
7699
8102
  }
7700
- function buildCodeReviewActionOptions(reviewState) {
8103
+ function buildCodeReviewActionOptions(reviewState, reviewSyncCommand = null) {
7701
8104
  if (reviewState === "merged") {
7702
8105
  return [
7703
8106
  buildStageOption(
7704
8107
  "A",
7705
8108
  "A",
7706
8109
  "review_sync_approved",
7707
- "Sync the already-merged PR state into tasks.md and pr.md before closing the feature."
8110
+ "Sync the already-merged PR state into tasks.md and pr.md before closing the feature.",
8111
+ reviewSyncCommand
7708
8112
  ),
7709
8113
  buildStageOption(
7710
8114
  "B",
@@ -7720,7 +8124,8 @@ function buildCodeReviewActionOptions(reviewState) {
7720
8124
  "A",
7721
8125
  "A",
7722
8126
  "review_sync_approved",
7723
- "Sync the approved PR review state into tasks.md and pr.md, then continue to the merge gate."
8127
+ "Sync the approved PR review state into tasks.md and pr.md, then continue to the merge gate.",
8128
+ reviewSyncCommand
7724
8129
  ),
7725
8130
  buildStageOption(
7726
8131
  "B",
@@ -8269,7 +8674,8 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
8269
8674
  if (requirements.requireReview && (!reviewApprovedInDocs || currentReviewState !== "approved")) {
8270
8675
  const reviewFixAllowed = currentReviewState === "changes_requested";
8271
8676
  const reviewApprovalRequired = !reviewFixAllowed;
8272
- const reviewActionOptions = reviewApprovalRequired ? buildCodeReviewActionOptions(currentReviewState) : void 0;
8677
+ const reviewSyncCommand = currentReviewState === "merged" ? `npx lee-spec-kit github pr ${buildFeatureArgs(feature)} --merge --confirm OK` : null;
8678
+ const reviewActionOptions = reviewApprovalRequired ? buildCodeReviewActionOptions(currentReviewState, reviewSyncCommand) : void 0;
8273
8679
  const reviewSummary = currentReviewState === "approved" ? "Record the approved PR review state in tasks.md and pr.md before proceeding to merge." : currentReviewState === "merged" ? "Sync the already-merged PR state into tasks.md and pr.md before marking the workflow as complete." : currentReviewState === "changes_requested" ? "Address the requested review changes and update the PR review evidence/decision before continuing." : currentReviewState === "review_pending_latest_commit" ? "Wait for a fresh review on the latest PR commit before taking the next review action." : currentReviewState === "review_rate_limited" ? "Wait for the current CodeRabbit review rate limit to clear, then re-check the latest PR review state before continuing." : currentReviewState === "draft" ? "Resolve the draft PR state before continuing to the merge boundary." : currentReviewState === "merge_blocked" ? "Resolve the current PR merge blocker before continuing to merge." : "Wait for PR review or inspect the current review state before taking the next review action.";
8274
8680
  return {
8275
8681
  status: "ok",
@@ -8280,7 +8686,8 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
8280
8686
  nextAction: buildAction(
8281
8687
  "code_review",
8282
8688
  reviewSummary,
8283
- reviewApprovalRequired
8689
+ reviewApprovalRequired,
8690
+ reviewSyncCommand
8284
8691
  ),
8285
8692
  approvalRequired: reviewApprovalRequired,
8286
8693
  implementationAllowed: reviewFixAllowed,
@@ -8702,7 +9109,20 @@ async function collectWorkflowAudit(cwd) {
8702
9109
  const allMeaningfulFeatureDocPaths = meaningfulChangedFeatureDocPaths;
8703
9110
  const latestCodeChangeAt = await getLatestMtimeIso(combinedChangedCodePaths);
8704
9111
  const latestFeatureDocSyncAt = await getLatestWorkflowSyncMarkerAt(activeFeature);
9112
+ const duplicateWorkflowSyncMarkerPaths = await collectDuplicateWorkflowSyncMarkerPaths(activeFeature);
8705
9113
  if (combinedChangedCodePaths.length === 0) {
9114
+ if (duplicateWorkflowSyncMarkerPaths.length > 0) {
9115
+ return {
9116
+ status: "needs_sync",
9117
+ reasonCode: "DUPLICATE_WORKFLOW_SYNC_MARKERS",
9118
+ docsDir: config.docsDir,
9119
+ activeFeatureRef,
9120
+ changedCodePaths: [],
9121
+ changedFeatureDocPaths: duplicateWorkflowSyncMarkerPaths,
9122
+ latestCodeChangeAt: null,
9123
+ latestFeatureDocSyncAt
9124
+ };
9125
+ }
8706
9126
  return {
8707
9127
  status: "ok",
8708
9128
  reasonCode: "WORKFLOW_IN_SYNC",
@@ -8714,6 +9134,18 @@ async function collectWorkflowAudit(cwd) {
8714
9134
  latestFeatureDocSyncAt
8715
9135
  };
8716
9136
  }
9137
+ if (duplicateWorkflowSyncMarkerPaths.length > 0) {
9138
+ return {
9139
+ status: "needs_sync",
9140
+ reasonCode: "DUPLICATE_WORKFLOW_SYNC_MARKERS",
9141
+ docsDir: config.docsDir,
9142
+ activeFeatureRef,
9143
+ changedCodePaths: combinedChangedCodePaths.map((item) => item.relativeToRepo),
9144
+ changedFeatureDocPaths: duplicateWorkflowSyncMarkerPaths,
9145
+ latestCodeChangeAt,
9146
+ latestFeatureDocSyncAt
9147
+ };
9148
+ }
8717
9149
  if (!activeFeatureRef) {
8718
9150
  return {
8719
9151
  status: "needs_sync",
@@ -8839,6 +9271,23 @@ async function getLatestWorkflowSyncMarkerAt(activeFeature) {
8839
9271
  }
8840
9272
  return latest > 0 ? new Date(latest).toISOString() : null;
8841
9273
  }
9274
+ async function collectDuplicateWorkflowSyncMarkerPaths(activeFeature) {
9275
+ if (!activeFeature) return [];
9276
+ const canonicalFiles = ["spec.md", "plan.md", "tasks.md", "decisions.md", "issue.md", "pr.md"];
9277
+ const markerPaths = [];
9278
+ let markerCount = 0;
9279
+ for (const fileName of canonicalFiles) {
9280
+ const absolutePath = path8.join(activeFeature.path, fileName);
9281
+ if (!await fs.pathExists(absolutePath)) continue;
9282
+ const content = await fs.readFile(absolutePath, "utf-8");
9283
+ const matches = [...content.matchAll(WORKFLOW_SYNC_MARKER_PATTERN)];
9284
+ if (matches.length > 0) {
9285
+ markerCount += matches.length;
9286
+ markerPaths.push(normalizeSlashes2(path8.relative(activeFeature.path, absolutePath)));
9287
+ }
9288
+ }
9289
+ return markerCount > 1 ? markerPaths : [];
9290
+ }
8842
9291
  function extractWorkflowSyncMarkerTimes(content, nowMs, fileMtimeMs) {
8843
9292
  const values = [];
8844
9293
  for (const match of content.matchAll(WORKFLOW_SYNC_MARKER_PATTERN)) {
@@ -9224,10 +9673,10 @@ function parseStagedPaths(output) {
9224
9673
  staged.set(`path:${normalizeSlashes3(parts[1])}`, `${status}:path`);
9225
9674
  }
9226
9675
  return [...staged.entries()].map(([encodedPath, encodedStatus]) => {
9227
- const [role, path26] = encodedPath.split(":", 2);
9676
+ const [role, path27] = encodedPath.split(":", 2);
9228
9677
  const [status, entryRole] = encodedStatus.split(":", 2);
9229
9678
  return {
9230
- path: path26,
9679
+ path: path27,
9231
9680
  status,
9232
9681
  role: entryRole || role || "path"
9233
9682
  };
@@ -9502,6 +9951,8 @@ function configureRootCommandSurface() {
9502
9951
  ["init", "Docs Schema Commands:"],
9503
9952
  ["idea", "Docs Schema Commands:"],
9504
9953
  ["feature", "Docs Schema Commands:"],
9954
+ ["task", "Docs Schema Commands:"],
9955
+ ["decision", "Docs Schema Commands:"],
9505
9956
  ["docs", "Workflow Policy Commands:"],
9506
9957
  ["detect", "Workflow Policy Commands:"],
9507
9958
  ["github", "Workflow Policy Commands:"],
@@ -9531,6 +9982,8 @@ updateCommand(program);
9531
9982
  configCommand(program);
9532
9983
  githubCommand(program);
9533
9984
  docsCommand(program);
9985
+ taskCommand(program);
9986
+ decisionCommand(program);
9534
9987
  detectCommand(program);
9535
9988
  workflowStageCommand(program);
9536
9989
  integrationsCommand(program);