lee-spec-kit 0.8.4 → 0.8.6

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
@@ -1299,7 +1299,7 @@ Before taking the next workflow step:
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
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
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
+ 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
1303
1303
 
1304
1304
  Approval and remote actions:
1305
1305
 
@@ -1314,7 +1314,7 @@ Approval and remote actions:
1314
1314
  Validation:
1315
1315
 
1316
1316
  - Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation
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 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
1318
1318
  `;
1319
1319
  function renderManagedSegment(lang, docsRepo) {
1320
1320
  return `${LEE_SPEC_KIT_AGENTS_BEGIN}
@@ -5433,10 +5433,10 @@ function resolvePrClosingIssueNumber(tasksContent, featureIssueNumber, lang) {
5433
5433
  return issueNumber;
5434
5434
  }
5435
5435
  function assertRemoteIssueExists(issueNumber, cwd, lang) {
5436
- if (!issueNumber) return;
5436
+ if (!issueNumber) return null;
5437
5437
  const result = runProcess(
5438
5438
  "gh",
5439
- ["issue", "view", issueNumber, "--json", "number,state"],
5439
+ ["issue", "view", issueNumber, "--json", "number,state,title"],
5440
5440
  cwd
5441
5441
  );
5442
5442
  if (result.code !== 0) {
@@ -5473,6 +5473,7 @@ function assertRemoteIssueExists(issueNumber, cwd, lang) {
5473
5473
  })
5474
5474
  );
5475
5475
  }
5476
+ return payload;
5476
5477
  }
5477
5478
  function getRequiredIssueSections(lang) {
5478
5479
  return getGithubDraftRequiredSections("issue", lang);
@@ -6227,16 +6228,18 @@ function githubCommand(program2) {
6227
6228
  feature.issueNumber ? String(feature.issueNumber) : void 0,
6228
6229
  config.lang
6229
6230
  );
6230
- title = closingIssueNumber && closingIssueNumber.trim() ? defaultTitle : requestedTitle || defaultTitle;
6231
- assertRemoteIssueExists(
6231
+ const remoteIssue = assertRemoteIssueExists(
6232
6232
  closingIssueNumber,
6233
6233
  projectGitCwd,
6234
6234
  config.lang
6235
6235
  );
6236
- if (closingIssueNumber && options.title?.trim() && options.title.trim() !== defaultTitle) {
6236
+ const linkedIssueTitle = remoteIssue?.title?.trim() || "";
6237
+ const issueLinkedDefaultTitle = linkedIssueTitle || defaultTitle;
6238
+ title = closingIssueNumber && closingIssueNumber.trim() ? issueLinkedDefaultTitle : requestedTitle || defaultTitle;
6239
+ if (closingIssueNumber && options.title?.trim() && options.title.trim() !== issueLinkedDefaultTitle) {
6237
6240
  throw createCliError(
6238
6241
  "PRECONDITION_FAILED",
6239
- `PR title must follow the existing convention: "${defaultTitle}".`
6242
+ `PR title must match the linked issue title: "${issueLinkedDefaultTitle}".`
6240
6243
  );
6241
6244
  }
6242
6245
  const normalizedBody = ensureIssueClosingLine(
@@ -7887,7 +7890,7 @@ function buildPostMergeCleanupCommand(state) {
7887
7890
  }
7888
7891
  if (state.worktreePath) {
7889
7892
  commandParts.push(
7890
- `if [ -d "${state.worktreePath}" ]; then git -C "${state.projectRootGitCwd}" worktree remove "${state.worktreePath}"; fi`
7893
+ `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`
7891
7894
  );
7892
7895
  }
7893
7896
  if (state.headBranch) {
@@ -7896,7 +7899,7 @@ function buildPostMergeCleanupCommand(state) {
7896
7899
  );
7897
7900
  if (state.hasOriginRemote) {
7898
7901
  commandParts.push(
7899
- `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`
7902
+ `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`
7900
7903
  );
7901
7904
  commandParts.push(
7902
7905
  `git -C "${state.projectRootGitCwd}" fetch --prune origin`
@@ -8100,14 +8103,15 @@ function resolveCurrentReviewState(tasks, prDraft, remoteReviewState) {
8100
8103
  }
8101
8104
  return "unknown";
8102
8105
  }
8103
- function buildCodeReviewActionOptions(reviewState) {
8106
+ function buildCodeReviewActionOptions(reviewState, reviewSyncCommand = null) {
8104
8107
  if (reviewState === "merged") {
8105
8108
  return [
8106
8109
  buildStageOption(
8107
8110
  "A",
8108
8111
  "A",
8109
8112
  "review_sync_approved",
8110
- "Sync the already-merged PR state into tasks.md and pr.md before closing the feature."
8113
+ "Sync the already-merged PR state into tasks.md and pr.md before closing the feature.",
8114
+ reviewSyncCommand
8111
8115
  ),
8112
8116
  buildStageOption(
8113
8117
  "B",
@@ -8123,7 +8127,8 @@ function buildCodeReviewActionOptions(reviewState) {
8123
8127
  "A",
8124
8128
  "A",
8125
8129
  "review_sync_approved",
8126
- "Sync the approved PR review state into tasks.md and pr.md, then continue to the merge gate."
8130
+ "Sync the approved PR review state into tasks.md and pr.md, then continue to the merge gate.",
8131
+ reviewSyncCommand
8127
8132
  ),
8128
8133
  buildStageOption(
8129
8134
  "B",
@@ -8672,7 +8677,8 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
8672
8677
  if (requirements.requireReview && (!reviewApprovedInDocs || currentReviewState !== "approved")) {
8673
8678
  const reviewFixAllowed = currentReviewState === "changes_requested";
8674
8679
  const reviewApprovalRequired = !reviewFixAllowed;
8675
- const reviewActionOptions = reviewApprovalRequired ? buildCodeReviewActionOptions(currentReviewState) : void 0;
8680
+ const reviewSyncCommand = currentReviewState === "merged" ? `npx lee-spec-kit github pr ${buildFeatureArgs(feature)} --merge --confirm OK` : null;
8681
+ const reviewActionOptions = reviewApprovalRequired ? buildCodeReviewActionOptions(currentReviewState, reviewSyncCommand) : void 0;
8676
8682
  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.";
8677
8683
  return {
8678
8684
  status: "ok",
@@ -8683,7 +8689,8 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
8683
8689
  nextAction: buildAction(
8684
8690
  "code_review",
8685
8691
  reviewSummary,
8686
- reviewApprovalRequired
8692
+ reviewApprovalRequired,
8693
+ reviewSyncCommand
8687
8694
  ),
8688
8695
  approvalRequired: reviewApprovalRequired,
8689
8696
  implementationAllowed: reviewFixAllowed,
@@ -9105,7 +9112,20 @@ async function collectWorkflowAudit(cwd) {
9105
9112
  const allMeaningfulFeatureDocPaths = meaningfulChangedFeatureDocPaths;
9106
9113
  const latestCodeChangeAt = await getLatestMtimeIso(combinedChangedCodePaths);
9107
9114
  const latestFeatureDocSyncAt = await getLatestWorkflowSyncMarkerAt(activeFeature);
9115
+ const duplicateWorkflowSyncMarkerPaths = await collectDuplicateWorkflowSyncMarkerPaths(activeFeature);
9108
9116
  if (combinedChangedCodePaths.length === 0) {
9117
+ if (duplicateWorkflowSyncMarkerPaths.length > 0) {
9118
+ return {
9119
+ status: "needs_sync",
9120
+ reasonCode: "DUPLICATE_WORKFLOW_SYNC_MARKERS",
9121
+ docsDir: config.docsDir,
9122
+ activeFeatureRef,
9123
+ changedCodePaths: [],
9124
+ changedFeatureDocPaths: duplicateWorkflowSyncMarkerPaths,
9125
+ latestCodeChangeAt: null,
9126
+ latestFeatureDocSyncAt
9127
+ };
9128
+ }
9109
9129
  return {
9110
9130
  status: "ok",
9111
9131
  reasonCode: "WORKFLOW_IN_SYNC",
@@ -9117,6 +9137,18 @@ async function collectWorkflowAudit(cwd) {
9117
9137
  latestFeatureDocSyncAt
9118
9138
  };
9119
9139
  }
9140
+ if (duplicateWorkflowSyncMarkerPaths.length > 0) {
9141
+ return {
9142
+ status: "needs_sync",
9143
+ reasonCode: "DUPLICATE_WORKFLOW_SYNC_MARKERS",
9144
+ docsDir: config.docsDir,
9145
+ activeFeatureRef,
9146
+ changedCodePaths: combinedChangedCodePaths.map((item) => item.relativeToRepo),
9147
+ changedFeatureDocPaths: duplicateWorkflowSyncMarkerPaths,
9148
+ latestCodeChangeAt,
9149
+ latestFeatureDocSyncAt
9150
+ };
9151
+ }
9120
9152
  if (!activeFeatureRef) {
9121
9153
  return {
9122
9154
  status: "needs_sync",
@@ -9242,6 +9274,23 @@ async function getLatestWorkflowSyncMarkerAt(activeFeature) {
9242
9274
  }
9243
9275
  return latest > 0 ? new Date(latest).toISOString() : null;
9244
9276
  }
9277
+ async function collectDuplicateWorkflowSyncMarkerPaths(activeFeature) {
9278
+ if (!activeFeature) return [];
9279
+ const canonicalFiles = ["spec.md", "plan.md", "tasks.md", "decisions.md", "issue.md", "pr.md"];
9280
+ const markerPaths = [];
9281
+ let markerCount = 0;
9282
+ for (const fileName of canonicalFiles) {
9283
+ const absolutePath = path8.join(activeFeature.path, fileName);
9284
+ if (!await fs.pathExists(absolutePath)) continue;
9285
+ const content = await fs.readFile(absolutePath, "utf-8");
9286
+ const matches = [...content.matchAll(WORKFLOW_SYNC_MARKER_PATTERN)];
9287
+ if (matches.length > 0) {
9288
+ markerCount += matches.length;
9289
+ markerPaths.push(normalizeSlashes2(path8.relative(activeFeature.path, absolutePath)));
9290
+ }
9291
+ }
9292
+ return markerCount > 1 ? markerPaths : [];
9293
+ }
9245
9294
  function extractWorkflowSyncMarkerTimes(content, nowMs, fileMtimeMs) {
9246
9295
  const values = [];
9247
9296
  for (const match of content.matchAll(WORKFLOW_SYNC_MARKER_PATTERN)) {