lee-spec-kit 0.8.0 → 0.8.1

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { hasLeeSpecKitCodexBootstrap } from './chunk-RYSDBL6X.js';
2
+ import { hasLeeSpecKitCodexBootstrap } from './chunk-LYFRLOFQ.js';
3
3
  import { runGitCapture, runGitOrThrow } from './chunk-GR7JQBWF.js';
4
4
  import { __dirname as __dirname$1 } from './chunk-7V7RMGEU.js';
5
5
  import { program } from 'commander';
@@ -1282,17 +1282,24 @@ On session start or after context compression/reset:
1282
1282
  3. Read any unread \`requiredDocs[*].command\` from that output
1283
1283
  4. Cache built-in docs per session and only re-read them when the user explicitly asks for a policy refresh, \`npx lee-spec-kit update\` changed the policy, or the session restarted
1284
1284
 
1285
- Before implementing or editing:
1285
+ Before taking the next workflow step:
1286
1286
 
1287
1287
  1. Confirm the active feature from the request, docs tree, issue/PR context, or the most recently active feature folder
1288
1288
  2. Read the active feature docs as the SSOT: \`spec.md\`, \`plan.md\`, \`tasks.md\`, and \`decisions.md\`
1289
1289
  3. When relevant, also read \`issue.md\` and \`pr.md\`
1290
- 4. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
1291
- 5. 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
1290
+ 4. Run \`npx lee-spec-kit workflow-stage <feature-ref> --json\` and follow only the returned \`nextAction\`
1291
+ 5. Do not start implementation unless \`stage === "implementation"\` and \`implementationAllowed === true\`
1292
+ 6. Treat stages before implementation as hard gates:
1293
+ - spec / plan / tasks approvals
1294
+ - issue preparation / issue creation
1295
+ - branch creation
1296
+ 7. Keep docs and code synchronized; if code changes materially, update the active feature docs in the same turn before stopping
1297
+ 8. 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
1292
1298
 
1293
1299
  Approval and remote actions:
1294
1300
 
1295
1301
  - Ask the user for approval only at documented workflow approval boundaries or before remote/destructive actions
1302
+ - If \`workflow-stage --json\` reports \`approvalRequired === true\`, stop at that boundary and ask the user before proceeding
1296
1303
  - Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\` when hooks or manual checks need commit-time docs path enforcement
1297
1304
  - Before remote GitHub actions, share the plan or artifact being sent
1298
1305
  - Respect repo policy from docs and config first; hooks only enforce guardrails and continuation checks
@@ -2476,16 +2483,10 @@ async function runInit(options) {
2476
2483
  "Standalone workspace root could not be resolved. Re-run init from the shared workspace root above the docs directory."
2477
2484
  );
2478
2485
  }
2479
- await upsertLeeSpecKitAgentsMd(path8.join(targetDir, "AGENTS.md"), {
2486
+ await upsertLeeSpecKitAgentsMd(path8.join(standaloneWorkspaceRoot, "AGENTS.md"), {
2480
2487
  lang,
2481
2488
  docsRepo
2482
2489
  });
2483
- if (standaloneWorkspaceRoot !== path8.resolve(targetDir)) {
2484
- await upsertLeeSpecKitAgentsMd(path8.join(standaloneWorkspaceRoot, "AGENTS.md"), {
2485
- lang,
2486
- docsRepo
2487
- });
2488
- }
2489
2490
  }
2490
2491
  } catch {
2491
2492
  }
@@ -3438,7 +3439,6 @@ async function collectAgentsMdTargets(cwd, config) {
3438
3439
  targets.add(path8.join(repoRoot, "AGENTS.md"));
3439
3440
  return [...targets];
3440
3441
  }
3441
- targets.add(path8.join(config.docsDir, "AGENTS.md"));
3442
3442
  const workspaceRoot = resolveConfiguredStandaloneWorkspaceRoot(config);
3443
3443
  if (!workspaceRoot) {
3444
3444
  throw createCliError(
@@ -3446,9 +3446,7 @@ async function collectAgentsMdTargets(cwd, config) {
3446
3446
  "Standalone workspaceRoot is missing or invalid. Run `npx lee-spec-kit update --agents-md` from the shared workspace root to migrate this project."
3447
3447
  );
3448
3448
  }
3449
- if (workspaceRoot !== path8.resolve(config.docsDir)) {
3450
- targets.add(path8.join(workspaceRoot, "AGENTS.md"));
3451
- }
3449
+ targets.add(path8.join(workspaceRoot, "AGENTS.md"));
3452
3450
  return [...targets];
3453
3451
  }
3454
3452
  function isPlainObject(value) {
@@ -6738,7 +6736,7 @@ function registerCodexIntegration(parent) {
6738
6736
  getCodexConfigPath,
6739
6737
  removeLeeSpecKitCodexBootstrap,
6740
6738
  upsertLeeSpecKitCodexBootstrap
6741
- } = await import('./bootstrap-ZIJP7O72.js');
6739
+ } = await import('./bootstrap-G37N6RGB.js');
6742
6740
  const filePath = getCodexConfigPath();
6743
6741
  if (options.remove) {
6744
6742
  const result2 = await removeLeeSpecKitCodexBootstrap(filePath);
@@ -6783,7 +6781,7 @@ function registerCodexHooksIntegration(parent) {
6783
6781
  removeLeeSpecKitCodexHooks,
6784
6782
  resolveCodexHooksRepoRoot,
6785
6783
  upsertLeeSpecKitCodexHooks
6786
- } = await import('./hooks-IP6FICAV.js');
6784
+ } = await import('./hooks-4S33YUIB.js');
6787
6785
  const repoRoot = config.docsRepo === "standalone" ? resolveConfiguredStandaloneWorkspaceRoot(config) : resolveCodexHooksRepoRoot(process.cwd());
6788
6786
  if (!repoRoot) {
6789
6787
  throw createCliError(
@@ -6818,6 +6816,529 @@ function integrationsCommand(program2) {
6818
6816
  registerCodexIntegration(integrations);
6819
6817
  registerCodexHooksIntegration(integrations);
6820
6818
  }
6819
+ var DOC_STATUS_LABELS = ["Doc Status", "\uBB38\uC11C \uC0C1\uD0DC"];
6820
+ var ISSUE_LABELS = ["Issue", "Issue Number", "\uC774\uC288", "\uC774\uC288 \uBC88\uD638"];
6821
+ var BRANCH_LABELS = ["Branch", "\uBE0C\uB79C\uCE58"];
6822
+ var PR_LABELS = ["PR", "Pull Request"];
6823
+ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
6824
+ var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
6825
+ var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
6826
+ var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
6827
+ function resolveWorkflowRequirements(config) {
6828
+ const workflow = config.workflow || {};
6829
+ const workflowMode = workflow.mode || workflow.preset || "github";
6830
+ const isLocalWorkflow = workflowMode === "local";
6831
+ return {
6832
+ requireIssue: workflow.requireIssue ?? !isLocalWorkflow,
6833
+ requireBranch: workflow.requireBranch ?? true,
6834
+ requirePr: workflow.requirePr ?? !isLocalWorkflow,
6835
+ requireReview: workflow.requireReview ?? !isLocalWorkflow,
6836
+ requireMerge: workflow.requireMerge ?? !isLocalWorkflow,
6837
+ prePrReviewEnabled: workflow.prePrReview?.enabled ?? !isLocalWorkflow
6838
+ };
6839
+ }
6840
+ function parseApprovalStatus(raw) {
6841
+ const value = (raw || "").trim().toLowerCase();
6842
+ if (value === "draft") return "draft";
6843
+ if (value === "review") return "review";
6844
+ if (value === "approved") return "approved";
6845
+ return null;
6846
+ }
6847
+ function extractFieldValue(content, labels) {
6848
+ for (const label of Array.isArray(labels) ? labels : [labels]) {
6849
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6850
+ const match = content.match(
6851
+ new RegExp(`^\\s*-\\s*\\*\\*${escaped}\\*\\*:\\s*(.*?)\\s*$`, "mi")
6852
+ );
6853
+ if (!match) continue;
6854
+ const value = match[1].trim();
6855
+ if (value) return value;
6856
+ }
6857
+ return null;
6858
+ }
6859
+ function parseMarkdownCheckbox(line) {
6860
+ const match = line.match(/^\s*-\s*\[([ xX])\]\s+/);
6861
+ if (!match) return null;
6862
+ return match[1].toLowerCase() === "x";
6863
+ }
6864
+ function withoutFencedCodeBlocks(content) {
6865
+ const lines = [];
6866
+ let inFence = false;
6867
+ for (const line of content.split("\n")) {
6868
+ if (/^\s*```/.test(line)) {
6869
+ inFence = !inFence;
6870
+ continue;
6871
+ }
6872
+ if (!inFence) {
6873
+ lines.push(line);
6874
+ }
6875
+ }
6876
+ return lines;
6877
+ }
6878
+ function parseTasksDoc(content) {
6879
+ const issueRaw = extractFieldValue(content, ISSUE_LABELS);
6880
+ const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
6881
+ const issueNumber = issueNumberMatch ? Number(issueNumberMatch[1]) : null;
6882
+ const branchRaw = extractFieldValue(content, BRANCH_LABELS);
6883
+ const prRaw = extractFieldValue(content, PR_LABELS);
6884
+ const prePrDecision = extractFieldValue(content, PRE_PR_DECISION_LABELS);
6885
+ const tasks = [];
6886
+ const nonCodeLines = withoutFencedCodeBlocks(content);
6887
+ for (const line of nonCodeLines) {
6888
+ const match = line.match(
6889
+ /^\s*-\s*\[(TODO|DOING|DONE|REVIEW)\](?:\[[^\]]+\])*\s+(.+?)\s*$/i
6890
+ );
6891
+ if (!match) continue;
6892
+ tasks.push({
6893
+ raw: line,
6894
+ status: match[1].toUpperCase(),
6895
+ title: match[2].trim()
6896
+ });
6897
+ }
6898
+ const allTasksChecked = nonCodeLines.some(
6899
+ (line) => /(All tasks are|모든 태스크가)/i.test(line) && parseMarkdownCheckbox(line) === true
6900
+ );
6901
+ const testsChecked = nonCodeLines.some(
6902
+ (line) => /(Tests executed and passing|테스트 실행 및 통과)/i.test(line) && parseMarkdownCheckbox(line) === true
6903
+ );
6904
+ const finalOutcomeChecked = nonCodeLines.some(
6905
+ (line) => /(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i.test(
6906
+ line
6907
+ ) && parseMarkdownCheckbox(line) === true
6908
+ );
6909
+ const prStatus = (() => {
6910
+ const value = (extractFieldValue(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
6911
+ if (value === "review") return "review";
6912
+ if (value === "approved") return "approved";
6913
+ return null;
6914
+ })();
6915
+ const prePrReviewStatus = (() => {
6916
+ const value = (extractFieldValue(content, PRE_PR_REVIEW_LABELS) || "").trim().toLowerCase();
6917
+ if (value === "pending") return "pending";
6918
+ if (value === "running") return "running";
6919
+ if (value === "done") return "done";
6920
+ return null;
6921
+ })();
6922
+ const prePrDecisionOutcome = (() => {
6923
+ const value = (prePrDecision || "").trim().toLowerCase();
6924
+ const match = value.match(/\b(approve|changes_requested|blocked)\b/);
6925
+ return match?.[1] || null;
6926
+ })();
6927
+ return {
6928
+ docStatus: parseApprovalStatus(
6929
+ extractFieldValue(content, DOC_STATUS_LABELS) || void 0
6930
+ ),
6931
+ issueNumber,
6932
+ branch: sanitizeMetadataValue(branchRaw),
6933
+ prLink: sanitizeMetadataValue(prRaw),
6934
+ prStatus,
6935
+ prePrReviewStatus,
6936
+ prePrEvidence: sanitizeMetadataValue(
6937
+ extractFieldValue(content, PRE_PR_EVIDENCE_LABELS)
6938
+ ),
6939
+ prePrDecision: sanitizeMetadataValue(prePrDecision),
6940
+ prePrDecisionOutcome,
6941
+ tasks,
6942
+ completion: {
6943
+ allTasksChecked,
6944
+ testsChecked,
6945
+ finalOutcomeChecked
6946
+ }
6947
+ };
6948
+ }
6949
+ function sanitizeMetadataValue(value) {
6950
+ if (!value) return null;
6951
+ const trimmed = value.trim().replace(/^`(.+)`$/, "$1");
6952
+ if (!trimmed || trimmed === "-") return null;
6953
+ return trimmed;
6954
+ }
6955
+ function parseWorkflowDraftMetadataExtended(content) {
6956
+ const metadata = parseWorkflowDraftMetadata(content);
6957
+ const prStatusRaw = extractFieldValue(content, PR_STATUS_LABELS);
6958
+ const normalizedPrStatus = (prStatusRaw || "").trim().toLowerCase();
6959
+ return {
6960
+ ...metadata,
6961
+ issueRef: sanitizeMetadataValue(extractFieldValue(content, ISSUE_LABELS)),
6962
+ prRef: sanitizeMetadataValue(extractFieldValue(content, PR_LABELS)),
6963
+ prStatus: normalizedPrStatus === "review" ? "review" : normalizedPrStatus === "approved" ? "approved" : null
6964
+ };
6965
+ }
6966
+ async function readFileIfExists(filePath) {
6967
+ if (!await fs.pathExists(filePath)) return null;
6968
+ return fs.readFile(filePath, "utf-8");
6969
+ }
6970
+ function buildFeatureRef(feature) {
6971
+ return feature.folderName;
6972
+ }
6973
+ function buildFeatureArgs(feature) {
6974
+ return feature.type && feature.type !== "single" ? `${buildFeatureRef(feature)} --component ${feature.type}` : buildFeatureRef(feature);
6975
+ }
6976
+ function resolveExpectedBranch(feature, tasks) {
6977
+ if (tasks.branch) return tasks.branch;
6978
+ if (!tasks.issueNumber) return null;
6979
+ return `feat/${tasks.issueNumber}-${feature.slug}`;
6980
+ }
6981
+ function getCurrentProjectBranch(feature) {
6982
+ return runGitCapture(["branch", "--show-current"], feature.git.projectGitCwd) || runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], feature.git.projectGitCwd) || null;
6983
+ }
6984
+ function nextTodoTask(tasks) {
6985
+ return tasks.tasks.find((task) => task.status === "DOING") || tasks.tasks.find((task) => task.status === "TODO") || null;
6986
+ }
6987
+ function allTasksDone(tasks) {
6988
+ return tasks.tasks.length > 0 && tasks.tasks.every((task) => task.status === "DONE");
6989
+ }
6990
+ function prePrSatisfied(tasks) {
6991
+ return tasks.prePrReviewStatus === "done" && !!tasks.prePrEvidence && !!tasks.prePrDecision && tasks.prePrDecisionOutcome === "approve";
6992
+ }
6993
+ function issueExistsRemotely(issueNumber, feature) {
6994
+ if (!issueNumber) return false;
6995
+ const result = runProcess(
6996
+ "gh",
6997
+ ["issue", "view", String(issueNumber), "--json", "number"],
6998
+ feature.git.projectGitCwd
6999
+ );
7000
+ return result.code === 0;
7001
+ }
7002
+ function prExistsRemotely(prRef, feature) {
7003
+ if (!prRef) return false;
7004
+ const result = runProcess(
7005
+ "gh",
7006
+ ["pr", "view", prRef, "--json", "url"],
7007
+ feature.git.projectGitCwd
7008
+ );
7009
+ return result.code === 0;
7010
+ }
7011
+ function buildAction(category, summary, approvalRequired, command = null) {
7012
+ return {
7013
+ category,
7014
+ summary,
7015
+ approvalRequired,
7016
+ command
7017
+ };
7018
+ }
7019
+ function resolveFeatureSelectionError(selection) {
7020
+ const reasonCode = selection.status === "no_features" ? "NO_FEATURES" : "FEATURE_SELECTION_REQUIRED";
7021
+ return {
7022
+ status: "error",
7023
+ reasonCode,
7024
+ docsDir: selection.config.docsDir,
7025
+ featureRef: null,
7026
+ stage: null,
7027
+ nextAction: null,
7028
+ approvalRequired: false,
7029
+ implementationAllowed: false,
7030
+ blockedReasonCode: null
7031
+ };
7032
+ }
7033
+ async function collectWorkflowStage(cwd, selector, component) {
7034
+ const config = await getConfig(cwd);
7035
+ if (!config) {
7036
+ return {
7037
+ status: "error",
7038
+ reasonCode: "CONFIG_NOT_FOUND",
7039
+ docsDir: null,
7040
+ featureRef: null,
7041
+ stage: null,
7042
+ nextAction: null,
7043
+ approvalRequired: false,
7044
+ implementationAllowed: false,
7045
+ blockedReasonCode: null
7046
+ };
7047
+ }
7048
+ const selection = await resolveFeatureSelection(cwd, selector, component);
7049
+ if (selection.status !== "selected" || !selection.matchedFeature) {
7050
+ return resolveFeatureSelectionError(selection);
7051
+ }
7052
+ const feature = selection.matchedFeature;
7053
+ const requirements = resolveWorkflowRequirements(config);
7054
+ const paths = getFeatureDocPaths(feature);
7055
+ const specContent = await readFileIfExists(path8.join(config.docsDir, paths.specPath));
7056
+ const planContent = await readFileIfExists(path8.join(config.docsDir, paths.planPath));
7057
+ const tasksContent = await readFileIfExists(path8.join(config.docsDir, paths.tasksPath));
7058
+ const issueContent = await readFileIfExists(path8.join(config.docsDir, paths.issuePath));
7059
+ const prContent = await readFileIfExists(path8.join(config.docsDir, paths.prPath));
7060
+ const specStatus = parseApprovalStatus(
7061
+ extractFieldValue(specContent || "", ["Status", "\uC0C1\uD0DC"]) || void 0
7062
+ );
7063
+ const planStatus = parseApprovalStatus(
7064
+ extractFieldValue(planContent || "", ["Status", "\uC0C1\uD0DC"]) || void 0
7065
+ );
7066
+ const tasks = parseTasksDoc(tasksContent || "");
7067
+ const issueDraft = parseWorkflowDraftMetadataExtended(issueContent || "");
7068
+ const prDraft = parseWorkflowDraftMetadataExtended(prContent || "");
7069
+ if (specStatus !== "approved") {
7070
+ const approvalRequired = specStatus === "review";
7071
+ return {
7072
+ status: "ok",
7073
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7074
+ docsDir: config.docsDir,
7075
+ featureRef: buildFeatureRef(feature),
7076
+ stage: "spec",
7077
+ nextAction: buildAction(
7078
+ approvalRequired ? "spec_approve" : "spec_write",
7079
+ approvalRequired ? "Get user approval and update spec.md status to Approved." : "Write or refine spec.md until it is ready for approval.",
7080
+ approvalRequired
7081
+ ),
7082
+ approvalRequired,
7083
+ implementationAllowed: false,
7084
+ blockedReasonCode: "SPEC_NOT_APPROVED"
7085
+ };
7086
+ }
7087
+ if (planStatus !== "approved") {
7088
+ const approvalRequired = planStatus === "review";
7089
+ return {
7090
+ status: "ok",
7091
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7092
+ docsDir: config.docsDir,
7093
+ featureRef: buildFeatureRef(feature),
7094
+ stage: "plan",
7095
+ nextAction: buildAction(
7096
+ approvalRequired ? "plan_approve" : "plan_write",
7097
+ approvalRequired ? "Get user approval and update plan.md status to Approved." : "Write or refine plan.md until it is ready for approval.",
7098
+ approvalRequired
7099
+ ),
7100
+ approvalRequired,
7101
+ implementationAllowed: false,
7102
+ blockedReasonCode: "PLAN_NOT_APPROVED"
7103
+ };
7104
+ }
7105
+ if (tasks.tasks.length === 0 || tasks.docStatus !== "approved") {
7106
+ const approvalRequired = tasks.docStatus === "review";
7107
+ return {
7108
+ status: "ok",
7109
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7110
+ docsDir: config.docsDir,
7111
+ featureRef: buildFeatureRef(feature),
7112
+ stage: "tasks",
7113
+ nextAction: buildAction(
7114
+ approvalRequired ? "tasks_approve" : "tasks_write",
7115
+ approvalRequired ? "Get user approval and update tasks.md Doc Status to Approved." : "Add and refine tasks until tasks.md is execution-ready and Approved.",
7116
+ approvalRequired
7117
+ ),
7118
+ approvalRequired,
7119
+ implementationAllowed: false,
7120
+ blockedReasonCode: "TASKS_NOT_READY"
7121
+ };
7122
+ }
7123
+ if (requirements.requireIssue) {
7124
+ const issueReady = issueDraft.status === "ready";
7125
+ const issueCreated = tasks.issueNumber !== null && issueExistsRemotely(tasks.issueNumber, feature);
7126
+ if (!issueCreated || !issueReady) {
7127
+ return {
7128
+ status: "ok",
7129
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7130
+ docsDir: config.docsDir,
7131
+ featureRef: buildFeatureRef(feature),
7132
+ stage: "issue",
7133
+ nextAction: issueReady && !issueCreated ? buildAction(
7134
+ "issue_create",
7135
+ "Create the GitHub issue from issue.md and sync the issue number into tasks.md.",
7136
+ true,
7137
+ `npx lee-spec-kit github issue ${buildFeatureArgs(feature)} --create --confirm OK`
7138
+ ) : buildAction(
7139
+ "issue_prepare",
7140
+ "Prepare issue.md and set its Status to Ready before issue creation.",
7141
+ false
7142
+ ),
7143
+ approvalRequired: issueReady && !issueCreated,
7144
+ implementationAllowed: false,
7145
+ blockedReasonCode: "ISSUE_NOT_CREATED"
7146
+ };
7147
+ }
7148
+ }
7149
+ if (requirements.requireBranch && !allTasksDone(tasks)) {
7150
+ const expectedBranch = resolveExpectedBranch(feature, tasks);
7151
+ const currentBranch = getCurrentProjectBranch(feature);
7152
+ if (expectedBranch && currentBranch !== expectedBranch) {
7153
+ return {
7154
+ status: "ok",
7155
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7156
+ docsDir: config.docsDir,
7157
+ featureRef: buildFeatureRef(feature),
7158
+ stage: "branch",
7159
+ nextAction: buildAction(
7160
+ "branch_create",
7161
+ `Switch the project repo to ${expectedBranch} before implementation starts.`,
7162
+ false,
7163
+ `git checkout -b ${expectedBranch}`
7164
+ ),
7165
+ approvalRequired: false,
7166
+ implementationAllowed: false,
7167
+ blockedReasonCode: "BRANCH_NOT_READY"
7168
+ };
7169
+ }
7170
+ }
7171
+ if (!allTasksDone(tasks)) {
7172
+ const currentTask = nextTodoTask(tasks);
7173
+ return {
7174
+ status: "ok",
7175
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7176
+ docsDir: config.docsDir,
7177
+ featureRef: buildFeatureRef(feature),
7178
+ stage: "implementation",
7179
+ nextAction: buildAction(
7180
+ "task_execute",
7181
+ currentTask ? `Continue the next implementation task: ${currentTask.title}` : "Continue the active implementation task.",
7182
+ false
7183
+ ),
7184
+ approvalRequired: false,
7185
+ implementationAllowed: true,
7186
+ blockedReasonCode: null
7187
+ };
7188
+ }
7189
+ if (!tasks.completion.allTasksChecked || !tasks.completion.testsChecked || !tasks.completion.finalOutcomeChecked) {
7190
+ return {
7191
+ status: "ok",
7192
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7193
+ docsDir: config.docsDir,
7194
+ featureRef: buildFeatureRef(feature),
7195
+ stage: "implementation_approve",
7196
+ nextAction: buildAction(
7197
+ "implementation_approve",
7198
+ "Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
7199
+ true
7200
+ ),
7201
+ approvalRequired: true,
7202
+ implementationAllowed: false,
7203
+ blockedReasonCode: "IMPLEMENTATION_APPROVAL_REQUIRED"
7204
+ };
7205
+ }
7206
+ if (requirements.prePrReviewEnabled && !prePrSatisfied(tasks)) {
7207
+ return {
7208
+ status: "ok",
7209
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7210
+ docsDir: config.docsDir,
7211
+ featureRef: buildFeatureRef(feature),
7212
+ stage: "pre_pr_review",
7213
+ nextAction: buildAction(
7214
+ "pre_pr_review",
7215
+ "Run and record the Pre-PR review until tasks.md shows an approve decision with evidence.",
7216
+ false
7217
+ ),
7218
+ approvalRequired: false,
7219
+ implementationAllowed: false,
7220
+ blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
7221
+ };
7222
+ }
7223
+ if (requirements.requirePr) {
7224
+ const prReady = prDraft.status === "ready";
7225
+ const prCreated = !!tasks.prLink && prExistsRemotely(tasks.prLink, feature);
7226
+ if (!prCreated || !prReady) {
7227
+ return {
7228
+ status: "ok",
7229
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7230
+ docsDir: config.docsDir,
7231
+ featureRef: buildFeatureRef(feature),
7232
+ stage: "pr",
7233
+ nextAction: prReady && !prCreated ? buildAction(
7234
+ "pr_create",
7235
+ "Create the GitHub PR from pr.md and sync the PR metadata into tasks.md.",
7236
+ true,
7237
+ `npx lee-spec-kit github pr ${buildFeatureArgs(feature)} --create --confirm OK`
7238
+ ) : buildAction(
7239
+ "pr_prepare",
7240
+ "Prepare pr.md and set its Status to Ready before PR creation.",
7241
+ false
7242
+ ),
7243
+ approvalRequired: prReady && !prCreated,
7244
+ implementationAllowed: false,
7245
+ blockedReasonCode: "PR_NOT_CREATED"
7246
+ };
7247
+ }
7248
+ }
7249
+ if (requirements.requireReview && (tasks.prStatus !== "approved" || prDraft.prStatus !== "approved")) {
7250
+ return {
7251
+ status: "ok",
7252
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7253
+ docsDir: config.docsDir,
7254
+ featureRef: buildFeatureRef(feature),
7255
+ stage: "code_review",
7256
+ nextAction: buildAction(
7257
+ "code_review",
7258
+ "Complete PR review and record the final approved review state in tasks.md.",
7259
+ false
7260
+ ),
7261
+ approvalRequired: false,
7262
+ implementationAllowed: false,
7263
+ blockedReasonCode: "PR_REVIEW_NOT_APPROVED"
7264
+ };
7265
+ }
7266
+ if (requirements.requireMerge) {
7267
+ return {
7268
+ status: "ok",
7269
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7270
+ docsDir: config.docsDir,
7271
+ featureRef: buildFeatureRef(feature),
7272
+ stage: "merge",
7273
+ nextAction: buildAction(
7274
+ "pr_merge",
7275
+ "Merge the PR and sync the merged state back into tasks.md.",
7276
+ true,
7277
+ `npx lee-spec-kit github pr ${buildFeatureArgs(feature)} --merge --confirm OK`
7278
+ ),
7279
+ approvalRequired: true,
7280
+ implementationAllowed: false,
7281
+ blockedReasonCode: null
7282
+ };
7283
+ }
7284
+ return {
7285
+ status: "ok",
7286
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
7287
+ docsDir: config.docsDir,
7288
+ featureRef: buildFeatureRef(feature),
7289
+ stage: "done",
7290
+ nextAction: null,
7291
+ approvalRequired: false,
7292
+ implementationAllowed: false,
7293
+ blockedReasonCode: null
7294
+ };
7295
+ }
7296
+
7297
+ // src/commands/workflow-stage.ts
7298
+ function workflowStageCommand(program2) {
7299
+ program2.command("workflow-stage [feature-name]").description("Resolve the current high-level workflow stage for the active feature").option("--json", "Output JSON for agents and hooks").option("--component <component>", "Component name for multi projects").action(async (featureName, options) => {
7300
+ try {
7301
+ const payload = await collectWorkflowStage(
7302
+ process.cwd(),
7303
+ featureName,
7304
+ options.component
7305
+ );
7306
+ if (options.json) {
7307
+ console.log(JSON.stringify(payload, null, 2));
7308
+ return;
7309
+ }
7310
+ if (payload.status !== "ok") {
7311
+ console.log(`${payload.status}: ${payload.reasonCode}`);
7312
+ process.exitCode = 1;
7313
+ return;
7314
+ }
7315
+ console.log(`stage: ${payload.stage}`);
7316
+ if (payload.nextAction) {
7317
+ console.log(`next: ${payload.nextAction.category}`);
7318
+ }
7319
+ } catch (error) {
7320
+ const cliError = toCliError(error);
7321
+ if (options.json) {
7322
+ console.log(
7323
+ JSON.stringify(
7324
+ {
7325
+ status: "error",
7326
+ reasonCode: cliError.code,
7327
+ error: cliError.message
7328
+ },
7329
+ null,
7330
+ 2
7331
+ )
7332
+ );
7333
+ process.exitCode = 1;
7334
+ return;
7335
+ }
7336
+ process.stderr.write(`[${cliError.code}] ${cliError.message}
7337
+ `);
7338
+ process.exitCode = 1;
7339
+ }
7340
+ });
7341
+ }
6821
7342
  var FEATURE_DOC_FILE_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(spec|plan|tasks|decisions|issue|pr)\.md$/i;
6822
7343
  var CODE_FILE_PATTERN = /(^|\/)(Dockerfile|Makefile)$|\.(c|cc|cjs|cpp|cs|css|cts|go|h|hpp|html|java|js|json|jsx|kt|mjs|mts|php|py|rb|rs|scss|sh|sql|swift|ts|tsx|vue|yaml|yml|zsh)$/i;
6823
7344
  var WORKFLOW_SYNC_MARKER_PATTERN = /<!--\s*lee-spec-kit:workflow-sync\s+([0-9]{4}-[0-9]{2}-[0-9]{2}T[^ ]+?)\s*-->/gi;
@@ -7384,10 +7905,10 @@ function parseStagedPaths(output) {
7384
7905
  staged.set(`path:${normalizeSlashes3(parts[1])}`, `${status}:path`);
7385
7906
  }
7386
7907
  return [...staged.entries()].map(([encodedPath, encodedStatus]) => {
7387
- const [role, path25] = encodedPath.split(":", 2);
7908
+ const [role, path26] = encodedPath.split(":", 2);
7388
7909
  const [status, entryRole] = encodedStatus.split(":", 2);
7389
7910
  return {
7390
- path: path25,
7911
+ path: path26,
7391
7912
  status,
7392
7913
  role: entryRole || role || "path"
7393
7914
  };
@@ -7632,6 +8153,7 @@ function configureRootCommandSurface() {
7632
8153
  ["docs", "Workflow Policy Commands:"],
7633
8154
  ["detect", "Workflow Policy Commands:"],
7634
8155
  ["github", "Workflow Policy Commands:"],
8156
+ ["workflow-stage", "Workflow Policy Commands:"],
7635
8157
  ["integrations", "Codex Integration Commands:"],
7636
8158
  ["commit-audit", "Codex Integration Commands:"],
7637
8159
  ["workflow-audit", "Codex Integration Commands:"]
@@ -7658,6 +8180,7 @@ configCommand(program);
7658
8180
  githubCommand(program);
7659
8181
  docsCommand(program);
7660
8182
  detectCommand(program);
8183
+ workflowStageCommand(program);
7661
8184
  integrationsCommand(program);
7662
8185
  workflowAuditCommand(program);
7663
8186
  commitAuditCommand(program);