lee-spec-kit 0.9.1 → 0.9.3

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 CHANGED
@@ -79,7 +79,8 @@ The overall approach is influenced by [spec-kit](https://github.com/github/spec-
79
79
  - `integrations codex`: install/remove the optional global `[features].hooks` setting
80
80
  - `commit-audit --json`
81
81
  - `workflow-audit --json`
82
- - `local merge <feature-ref> --json`: integrate a completed local Feature using its configured fast-forward or squash strategy and run post-merge checks
82
+ - `local verify <feature-ref> --json`: run checks in the local Feature worktree and bind the result to its exact tip/tree
83
+ - `local merge <feature-ref> --json`: integrate a verified local Feature using its configured fast-forward or squash strategy
83
84
  - `local cleanup <feature-ref> --json`: remove the managed worktree and optionally delete the integrated Feature branch
84
85
 
85
86
  Supported modes:
package/README.md CHANGED
@@ -79,7 +79,8 @@ npx lee-spec-kit feature user-auth
79
79
  - `integrations codex`: 선택적 전역 `[features].hooks` 설정 설치/제거
80
80
  - `commit-audit --json`: hooks용 commit-time docs path + canonical commit subject validator
81
81
  - `workflow-audit --json`: hooks용 docs sync validator
82
- - `local merge <feature-ref> --json`: 완료된 local Feature 설정된 fast-forward 또는 squash 전략으로 base branch통합하고 사후 검증 실행
82
+ - `local verify <feature-ref> --json`: local Feature worktree에서 검사를 실행하고 결과를 정확한 tip/tree결속
83
+ - `local merge <feature-ref> --json`: 검증된 local Feature를 설정된 fast-forward 또는 squash 전략으로 base branch에 통합
83
84
  - `local cleanup <feature-ref> --json`: managed worktree 제거 및 설정에 따른 통합 완료 Feature 브랜치 삭제
84
85
 
85
86
  지원 모드:
package/dist/index.js CHANGED
@@ -1308,7 +1308,7 @@ Before taking the next workflow step:
1308
1308
  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
1309
1309
  13. When \`workflow-stage --json\` returns \`nextAction.category === "pre_pr_review"\` and \`nextAction.executor === "subagent"\`, delegate a fresh read-only review using the returned \`model\`, \`reasoningEffort\`, and \`onUnavailable\` policy; do not select or require a named review skill
1310
1310
  14. The Pre-PR review subagent returns findings without modifying code; the main agent remediates findings and records the actual reviewer metadata, reviewed diff scope, evidence, and final decision
1311
- 15. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves integration, verification, and cleanup and returns \`done\`
1311
+ 15. For a local workflow, do not report completion directly after implementation approval; follow the exact returned \`local verify\` / \`local merge\` / \`local cleanup\` commands until \`workflow-stage\` proves verification, integration, and cleanup and returns \`done\`; \`feature_remediation\` explicitly permits Feature worktree fixes
1312
1312
  16. In a \`local-ff\` or \`local-squash\` workflow, keep implementation approval and local merge approval distinct when \`local_merge\` is required: the first accepts the implementation, and the second authorizes the configured integration strategy, post-merge checks, and local cleanup
1313
1313
 
1314
1314
  Approval and remote actions:
@@ -2543,6 +2543,7 @@ async function runInit(options) {
2543
2543
  baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
2544
2544
  completionStrategy: "local-ff",
2545
2545
  deleteFeatureBranchAfterMerge: true,
2546
+ featureChecks: [],
2546
2547
  postMergeChecks: []
2547
2548
  } : {},
2548
2549
  prePrReview: {
@@ -3748,6 +3749,33 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
3748
3749
  workflow.deleteFeatureBranchAfterMerge = true;
3749
3750
  changedPaths.push("workflow.deleteFeatureBranchAfterMerge");
3750
3751
  }
3752
+ if (!Object.prototype.hasOwnProperty.call(workflow, "featureChecks")) {
3753
+ workflow.featureChecks = Array.isArray(workflow.postMergeChecks) ? workflow.postMergeChecks : [];
3754
+ workflow.postMergeChecks = [];
3755
+ changedPaths.push("workflow.featureChecks");
3756
+ changedPaths.push("workflow.postMergeChecks");
3757
+ }
3758
+ setIfMissing(workflow, "featureChecks", [], "workflow.featureChecks");
3759
+ if (!Array.isArray(workflow.featureChecks)) {
3760
+ workflow.featureChecks = [];
3761
+ changedPaths.push("workflow.featureChecks");
3762
+ } else {
3763
+ const normalizedFeatureChecks = workflow.featureChecks.flatMap(
3764
+ (value) => {
3765
+ if (!isPlainObject(value) || typeof value.command !== "string") {
3766
+ return [];
3767
+ }
3768
+ const command = value.command.trim();
3769
+ if (!command) return [];
3770
+ const args = Array.isArray(value.args) ? value.args.filter((arg) => typeof arg === "string") : [];
3771
+ return [{ command, ...args.length > 0 ? { args } : {} }];
3772
+ }
3773
+ );
3774
+ if (JSON.stringify(normalizedFeatureChecks) !== JSON.stringify(workflow.featureChecks)) {
3775
+ workflow.featureChecks = normalizedFeatureChecks;
3776
+ changedPaths.push("workflow.featureChecks");
3777
+ }
3778
+ }
3751
3779
  setIfMissing(workflow, "postMergeChecks", [], "workflow.postMergeChecks");
3752
3780
  if (!Array.isArray(workflow.postMergeChecks)) {
3753
3781
  workflow.postMergeChecks = [];
@@ -7693,9 +7721,17 @@ async function resolveLocalIntegrationContext(config, feature) {
7693
7721
  const evidenceTip = resolveRef(projectRoot, evidenceRef);
7694
7722
  const featureTree = featureTip ? resolveTree(projectRoot, featureTip) : null;
7695
7723
  const integratedTree = integratedCommit ? resolveTree(projectRoot, integratedCommit) : null;
7696
- const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && isAncestor(projectRoot, integratedCommit, `refs/heads/${baseBranch}`) && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
7724
+ const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && integratedCommit === baseTip && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
7697
7725
  const squashEvidencePresent = !!featureTip && evidenceTip === featureTip;
7698
7726
  const squashIntegrationComplete = squashCommitMatchesSource && squashEvidencePresent;
7727
+ const recordedStrategy = state?.strategy || "local-ff";
7728
+ const recordedIntegratedCommit = state?.integratedCommit || state?.mergedBaseTip || null;
7729
+ const baseContainsRecordedIntegration = !!recordedIntegratedCommit && isAncestor(
7730
+ projectRoot,
7731
+ recordedIntegratedCommit,
7732
+ `refs/heads/${baseBranch}`
7733
+ );
7734
+ const cleanedIntegrationStillValid = state?.status === "cleaned" && recordedStrategy === completionStrategy && state.featureTip === featureTip && baseContainsRecordedIntegration && (completionStrategy === "local-squash" ? state.integratedCommit === recordedIntegratedCommit && state.integratedTree === featureTree && squashEvidencePresent : state.mergedBaseTip === featureTip && baseContainsFeature);
7699
7735
  return {
7700
7736
  projectRoot,
7701
7737
  featureWorktree: effectiveFeatureWorktree,
@@ -7703,12 +7739,14 @@ async function resolveLocalIntegrationContext(config, feature) {
7703
7739
  baseBranch,
7704
7740
  featureBranch,
7705
7741
  featureTip,
7742
+ featureTree,
7706
7743
  baseTip,
7707
7744
  currentBranch,
7708
7745
  state,
7709
7746
  completionStrategy,
7710
7747
  baseContainsFeature,
7711
- integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : baseContainsFeature,
7748
+ integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : !!featureTip && baseTip === featureTip,
7749
+ cleanedIntegrationStillValid,
7712
7750
  evidenceRef,
7713
7751
  squashCommitMatchesSource,
7714
7752
  squashEvidencePresent,
@@ -7717,7 +7755,10 @@ async function resolveLocalIntegrationContext(config, feature) {
7717
7755
  docsClean: isClean(feature.git.docsGitCwd),
7718
7756
  managedFeatureWorktree,
7719
7757
  featureBranchExists: !!featureBranchTip,
7720
- postMergeChecks: normalizePostMergeChecks(config.workflow?.postMergeChecks),
7758
+ featureChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow.featureChecks) : normalizeWorkflowChecks(config.workflow?.postMergeChecks),
7759
+ // Compatibility: pre-0.9.2 postMergeChecks are treated as Feature checks.
7760
+ // Projects can opt into true post-integration checks by defining featureChecks.
7761
+ postMergeChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow?.postMergeChecks) : [],
7721
7762
  deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
7722
7763
  };
7723
7764
  }
@@ -7742,20 +7783,69 @@ async function readLocalIntegrationState(projectRoot, feature) {
7742
7783
  function localCleanupComplete(context) {
7743
7784
  return context.integrationComplete && context.currentBranch === context.baseBranch && !context.managedFeatureWorktree && (!context.deleteFeatureBranchAfterMerge || !context.featureBranchExists) && context.projectRootClean && context.docsClean && context.state?.status === "cleaned" && context.state.featureTip === context.featureTip && context.state.mergedBaseTip === context.baseTip;
7744
7785
  }
7745
- function runPostMergeChecks(projectRoot, checks) {
7786
+ function runLocalChecks(cwd, checks, targetSha, logDir, phase) {
7746
7787
  const results = [];
7788
+ fs.ensureDirSync(logDir);
7747
7789
  for (const check of checks) {
7748
7790
  const args = Array.isArray(check.args) ? check.args.map(String) : [];
7749
- const result = runProcess(check.command, args, projectRoot);
7791
+ const startedAt = /* @__PURE__ */ new Date();
7792
+ const result = runProcess(check.command, args, cwd);
7793
+ const finishedAt = /* @__PURE__ */ new Date();
7794
+ const runtime = {
7795
+ node: process.version,
7796
+ platform: process.platform,
7797
+ arch: process.arch,
7798
+ path: process.env.PATH || ""
7799
+ };
7800
+ const index = results.length + 1;
7801
+ const logPath = path8.join(logDir, `${phase}-${String(index).padStart(2, "0")}.log`);
7802
+ const log = [
7803
+ `phase: ${phase}`,
7804
+ `targetSha: ${targetSha}`,
7805
+ `cwd: ${cwd}`,
7806
+ `command: ${JSON.stringify([check.command, ...args])}`,
7807
+ `startedAt: ${startedAt.toISOString()}`,
7808
+ `finishedAt: ${finishedAt.toISOString()}`,
7809
+ `exitCode: ${result.code}`,
7810
+ `runtime: ${JSON.stringify(runtime)}`,
7811
+ "",
7812
+ "--- stdout ---",
7813
+ result.stdout,
7814
+ "--- stderr ---",
7815
+ result.stderr
7816
+ ].join("\n");
7817
+ fs.writeFileSync(logPath, log, { encoding: "utf-8", mode: 384 });
7750
7818
  results.push({
7751
7819
  command: check.command,
7752
7820
  args,
7753
- exitCode: result.code
7821
+ cwd,
7822
+ targetSha,
7823
+ startedAt: startedAt.toISOString(),
7824
+ finishedAt: finishedAt.toISOString(),
7825
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
7826
+ exitCode: result.code,
7827
+ runtime,
7828
+ ...preview(result.stdout) ? { stdoutPreview: preview(result.stdout) } : {},
7829
+ ...preview(result.stderr) ? { stderrPreview: preview(result.stderr) } : {},
7830
+ logPath
7754
7831
  });
7755
7832
  if (result.code !== 0) break;
7756
7833
  }
7757
7834
  return results;
7758
7835
  }
7836
+ function resolveLocalIntegrationLogDir(projectRoot, feature, targetSha) {
7837
+ return path8.join(
7838
+ path8.dirname(resolveStatePath(projectRoot, feature)),
7839
+ "logs",
7840
+ resolveFeatureStateKey(feature),
7841
+ targetSha.slice(0, 12)
7842
+ );
7843
+ }
7844
+ function preview(value) {
7845
+ const trimmed = value.trim();
7846
+ return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
7847
+ \u2026[truncated]` : trimmed;
7848
+ }
7759
7849
  function gitRun(cwd, args) {
7760
7850
  return runProcess("git", args, cwd);
7761
7851
  }
@@ -7847,7 +7937,7 @@ function resolveRegisteredBranchWorktree(projectRoot, branch) {
7847
7937
  }
7848
7938
  return null;
7849
7939
  }
7850
- function normalizePostMergeChecks(value) {
7940
+ function normalizeWorkflowChecks(value) {
7851
7941
  if (!Array.isArray(value)) return [];
7852
7942
  return value.filter(
7853
7943
  (entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
@@ -7866,6 +7956,11 @@ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
7866
7956
  var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
7867
7957
  var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
7868
7958
  var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
7959
+ var COMPLETION_MARKERS = {
7960
+ allTasks: "lee-spec-kit:completion:all-tasks",
7961
+ tests: "lee-spec-kit:completion:tests",
7962
+ finalOutcome: "lee-spec-kit:completion:final-outcome"
7963
+ };
7869
7964
  function resolveWorkflowRequirements(config) {
7870
7965
  const workflow = config.workflow || {};
7871
7966
  const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
@@ -7920,6 +8015,16 @@ function withoutFencedCodeBlocks(content) {
7920
8015
  }
7921
8016
  return lines;
7922
8017
  }
8018
+ function parseCompletionCheckbox(lines, marker, legacyPattern) {
8019
+ const markerToken = `<!-- ${marker} -->`;
8020
+ const markedLines = lines.filter((line) => line.includes(markerToken));
8021
+ if (markedLines.length > 0) {
8022
+ return markedLines.length === 1 && parseMarkdownCheckbox(markedLines[0]) === true;
8023
+ }
8024
+ return lines.some(
8025
+ (line) => legacyPattern.test(line) && parseMarkdownCheckbox(line) === true
8026
+ );
8027
+ }
7923
8028
  function parseTasksDoc(content) {
7924
8029
  const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
7925
8030
  const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
@@ -7940,16 +8045,20 @@ function parseTasksDoc(content) {
7940
8045
  title: match[2].trim()
7941
8046
  });
7942
8047
  }
7943
- const allTasksChecked = nonCodeLines.some(
7944
- (line) => /(All tasks are|모든 태스크가)/i.test(line) && parseMarkdownCheckbox(line) === true
8048
+ const allTasksChecked = parseCompletionCheckbox(
8049
+ nonCodeLines,
8050
+ COMPLETION_MARKERS.allTasks,
8051
+ /(All tasks are|모든 태스크가)/i
7945
8052
  );
7946
- const testsChecked = nonCodeLines.some(
7947
- (line) => /(Tests executed and passing|테스트 실행 및 통과)/i.test(line) && parseMarkdownCheckbox(line) === true
8053
+ const testsChecked = parseCompletionCheckbox(
8054
+ nonCodeLines,
8055
+ COMPLETION_MARKERS.tests,
8056
+ /(Tests executed and passing|테스트 실행 및 통과)/i
7948
8057
  );
7949
- const finalOutcomeChecked = nonCodeLines.some(
7950
- (line) => /(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i.test(
7951
- line
7952
- ) && parseMarkdownCheckbox(line) === true
8058
+ const finalOutcomeChecked = parseCompletionCheckbox(
8059
+ nonCodeLines,
8060
+ COMPLETION_MARKERS.finalOutcome,
8061
+ /(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i
7953
8062
  );
7954
8063
  const prStatus = (() => {
7955
8064
  const value = (extractFieldValue2(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
@@ -9129,8 +9238,62 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
9129
9238
  }
9130
9239
  if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
9131
9240
  const localState = await resolveLocalIntegrationContext(config, feature);
9241
+ const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
9132
9242
  const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
9243
+ if (localState.cleanedIntegrationStillValid) {
9244
+ return {
9245
+ status: "ok",
9246
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9247
+ docsDir: config.docsDir,
9248
+ featureRef: buildFeatureRef(feature),
9249
+ stage: "done",
9250
+ nextAction: null,
9251
+ approvalRequired: false,
9252
+ implementationAllowed: false,
9253
+ blockedReasonCode: null
9254
+ };
9255
+ }
9133
9256
  if (!localState.integrationComplete) {
9257
+ const featureVerified = !!localState.state && ["feature_verified", "merged", "verified", "cleaned"].includes(
9258
+ localState.state.status
9259
+ ) && localState.state.verifiedFeatureTip === localState.featureTip && localState.state.verifiedFeatureTree === localState.featureTree;
9260
+ const featureVerificationFailed = localState.state?.status === "feature_failed" && localState.state.featureTip === localState.featureTip;
9261
+ if (featureVerificationFailed) {
9262
+ return {
9263
+ status: "ok",
9264
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9265
+ docsDir: config.docsDir,
9266
+ featureRef: buildFeatureRef(feature),
9267
+ stage: "feature_remediation",
9268
+ nextAction: buildAction(
9269
+ "feature_remediation",
9270
+ `Fix the failed checks in ${localState.featureBranch} at ${localState.featureWorktree}, commit the remediation, then verify the new Feature tip. Re-run the command without code changes to retry an environmental failure.`,
9271
+ false,
9272
+ localVerifyCommand
9273
+ ),
9274
+ approvalRequired: false,
9275
+ implementationAllowed: true,
9276
+ blockedReasonCode: "FEATURE_REMEDIATION_REQUIRED"
9277
+ };
9278
+ }
9279
+ if (!featureVerified) {
9280
+ return {
9281
+ status: "ok",
9282
+ reasonCode: "WORKFLOW_STAGE_RESOLVED",
9283
+ docsDir: config.docsDir,
9284
+ featureRef: buildFeatureRef(feature),
9285
+ stage: "feature_verify",
9286
+ nextAction: buildAction(
9287
+ "feature_verify",
9288
+ `Run the configured Feature checks in ${localState.featureBranch} before integration and bind the result to its exact commit and tree.`,
9289
+ false,
9290
+ localVerifyCommand
9291
+ ),
9292
+ approvalRequired: false,
9293
+ implementationAllowed: false,
9294
+ blockedReasonCode: "FEATURE_VERIFICATION_REQUIRED"
9295
+ };
9296
+ }
9134
9297
  const approvalRequired = resolveActionApprovalRequired(
9135
9298
  config,
9136
9299
  "local_merge",
@@ -10711,6 +10874,14 @@ async function collectDocsAudit(cwd) {
10711
10874
  // src/commands/local.ts
10712
10875
  function localCommand(program2) {
10713
10876
  const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
10877
+ local.command("verify [feature-name]").description("Verify the exact Feature tip before local integration").option("--component <component>", "Component name for multi projects").option("--json", "Output JSON for agents and hooks").action(
10878
+ async (featureName, options) => {
10879
+ await runLocalAction(
10880
+ options,
10881
+ () => runLocalVerify(featureName, options)
10882
+ );
10883
+ }
10884
+ );
10714
10885
  local.command("merge [feature-name]").description("Integrate a completed local feature and verify the result").option("--component <component>", "Component name for multi projects").option(
10715
10886
  "--confirm <token>",
10716
10887
  "Approval token when local_merge requires approval"
@@ -10731,6 +10902,111 @@ function localCommand(program2) {
10731
10902
  }
10732
10903
  );
10733
10904
  }
10905
+ async function runLocalVerify(featureName, options) {
10906
+ const cwd = process.cwd();
10907
+ const stage = await collectWorkflowStage(cwd, featureName, options.component);
10908
+ if (stage.status !== "ok" || !stage.nextAction || !["feature_verify", "feature_remediation"].includes(stage.nextAction.category)) {
10909
+ return blocked(
10910
+ "LOCAL_FEATURE_VERIFY_STAGE_REQUIRED",
10911
+ stage.featureRef,
10912
+ "The active workflow is not at feature_verify or feature_remediation."
10913
+ );
10914
+ }
10915
+ const selection = await resolveFeatureSelection(
10916
+ cwd,
10917
+ featureName,
10918
+ options.component
10919
+ );
10920
+ if (selection.status !== "selected" || !selection.matchedFeature) {
10921
+ return blocked("FEATURE_SELECTION_REQUIRED", null);
10922
+ }
10923
+ const config = await getConfig(cwd);
10924
+ if (!config || config.workflow?.mode !== "local") {
10925
+ return blocked("LOCAL_WORKFLOW_REQUIRED", selection.matchedFeature.folderName);
10926
+ }
10927
+ const feature = selection.matchedFeature;
10928
+ const context = await resolveLocalIntegrationContext(config, feature);
10929
+ if (!context.featureTip || !context.featureTree) {
10930
+ return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
10931
+ }
10932
+ if (!context.featureWorktreeClean || !context.docsClean) {
10933
+ return blocked(
10934
+ "LOCAL_FEATURE_VERIFY_DIRTY_WORKTREE",
10935
+ feature.folderName,
10936
+ "Feature worktree and docs repo must be clean before verification."
10937
+ );
10938
+ }
10939
+ const targetTip = context.featureTip;
10940
+ const targetTree = context.featureTree;
10941
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10942
+ const logDir = resolveLocalIntegrationLogDir(
10943
+ context.projectRoot,
10944
+ feature,
10945
+ targetTip
10946
+ );
10947
+ const verification = runLocalChecks(
10948
+ context.featureWorktree,
10949
+ context.featureChecks,
10950
+ targetTip,
10951
+ logDir,
10952
+ "feature"
10953
+ );
10954
+ const refreshed = await resolveLocalIntegrationContext(config, feature);
10955
+ const changedDuringVerification = refreshed.featureTip !== targetTip || refreshed.featureTree !== targetTree || !refreshed.featureWorktreeClean;
10956
+ const failed = verification.some((entry) => entry.exitCode !== 0);
10957
+ const baseState = {
10958
+ version: 1,
10959
+ featureRef: feature.folderName,
10960
+ component: feature.type,
10961
+ baseBranch: context.baseBranch,
10962
+ featureBranch: context.featureBranch,
10963
+ featureTip: targetTip,
10964
+ mergedBaseTip: context.baseTip || targetTip,
10965
+ strategy: context.completionStrategy,
10966
+ status: failed || changedDuringVerification ? "feature_failed" : "feature_verified",
10967
+ mergedAt: context.state?.mergedAt || now,
10968
+ featureVerifiedAt: failed || changedDuringVerification ? null : now,
10969
+ verifiedAt: null,
10970
+ cleanedAt: null,
10971
+ ...failed || changedDuringVerification ? {} : {
10972
+ verifiedFeatureTip: targetTip,
10973
+ verifiedFeatureTree: targetTree
10974
+ },
10975
+ featureVerification: verification,
10976
+ postMergeVerification: [],
10977
+ verification
10978
+ };
10979
+ await writeLocalIntegrationState(context.projectRoot, feature, baseState);
10980
+ if (changedDuringVerification) {
10981
+ return {
10982
+ ...blocked(
10983
+ "LOCAL_FEATURE_CHANGED_DURING_VERIFICATION",
10984
+ feature.folderName,
10985
+ "The Feature tip, tree, or worktree changed while checks were running."
10986
+ ),
10987
+ featureTip: refreshed.featureTip,
10988
+ verification
10989
+ };
10990
+ }
10991
+ if (failed) {
10992
+ return {
10993
+ ...blocked("LOCAL_FEATURE_CHECK_FAILED", feature.folderName),
10994
+ featureTip: targetTip,
10995
+ verification
10996
+ };
10997
+ }
10998
+ return {
10999
+ status: "ok",
11000
+ reasonCode: "LOCAL_FEATURE_VERIFIED",
11001
+ featureRef: feature.folderName,
11002
+ baseBranch: context.baseBranch,
11003
+ featureBranch: context.featureBranch,
11004
+ featureTip: targetTip,
11005
+ baseTip: context.baseTip,
11006
+ completionStrategy: context.completionStrategy,
11007
+ verification
11008
+ };
11009
+ }
10734
11010
  async function runLocalAction(options, action) {
10735
11011
  try {
10736
11012
  const payload = await action();
@@ -10802,6 +11078,15 @@ async function runLocalMerge(featureName, options) {
10802
11078
  "Both the configured base branch and Feature tip must exist."
10803
11079
  );
10804
11080
  }
11081
+ if (!context.integrationComplete && (!context.state || !["feature_verified", "merged", "verified", "cleaned"].includes(
11082
+ context.state.status
11083
+ ) || context.state.verifiedFeatureTip !== context.featureTip || context.state.verifiedFeatureTree !== context.featureTree)) {
11084
+ return blocked(
11085
+ "LOCAL_FEATURE_VERIFICATION_REQUIRED",
11086
+ feature.folderName,
11087
+ "Verify the exact Feature tip before local integration."
11088
+ );
11089
+ }
10805
11090
  if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
10806
11091
  return blocked(
10807
11092
  "LOCAL_MERGE_DIRTY_WORKTREE",
@@ -10810,6 +11095,7 @@ async function runLocalMerge(featureName, options) {
10810
11095
  );
10811
11096
  }
10812
11097
  const now = (/* @__PURE__ */ new Date()).toISOString();
11098
+ const originalBaseTip = context.baseTip;
10813
11099
  const previousStateStrategy = context.state?.strategy || "local-ff";
10814
11100
  let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
10815
11101
  version: 1,
@@ -10882,6 +11168,7 @@ async function runLocalMerge(featureName, options) {
10882
11168
  mergedBaseTip: mergedBaseTip || context.featureTip,
10883
11169
  strategy: context.completionStrategy,
10884
11170
  integratedCommit: mergedBaseTip || context.featureTip,
11171
+ originalBaseTip,
10885
11172
  ...integratedTree ? { integratedTree } : {},
10886
11173
  ...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
10887
11174
  status: "merged",
@@ -10896,16 +11183,36 @@ async function runLocalMerge(featureName, options) {
10896
11183
  if (!context.integrationComplete) {
10897
11184
  return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
10898
11185
  }
10899
- const verification = runPostMergeChecks(
11186
+ const integratedTip = resolveRef(
10900
11187
  context.projectRoot,
10901
- context.postMergeChecks
11188
+ `refs/heads/${context.baseBranch}`
10902
11189
  );
10903
- const failedCheck = verification.find((entry) => entry.exitCode !== 0);
11190
+ const verificationTarget = integratedTip || state.featureTip;
11191
+ const postMergeVerification = runLocalChecks(
11192
+ context.projectRoot,
11193
+ context.postMergeChecks,
11194
+ verificationTarget,
11195
+ resolveLocalIntegrationLogDir(
11196
+ context.projectRoot,
11197
+ feature,
11198
+ verificationTarget
11199
+ ),
11200
+ "post-merge"
11201
+ );
11202
+ const featureVerification = state.featureVerification || context.state?.featureVerification || [];
11203
+ const verification = [...featureVerification, ...postMergeVerification];
11204
+ const failedCheck = postMergeVerification.find((entry) => entry.exitCode !== 0);
10904
11205
  if (failedCheck) {
11206
+ rollbackLocalIntegration(context, originalBaseTip);
10905
11207
  await writeLocalIntegrationState(context.projectRoot, feature, {
10906
11208
  ...state,
10907
- status: "merged",
11209
+ status: "feature_failed",
11210
+ mergedBaseTip: originalBaseTip,
11211
+ integratedCommit: void 0,
11212
+ integratedTree: void 0,
10908
11213
  verification,
11214
+ featureVerification,
11215
+ postMergeVerification,
10909
11216
  verifiedAt: null
10910
11217
  });
10911
11218
  return {
@@ -10931,7 +11238,9 @@ async function runLocalMerge(featureName, options) {
10931
11238
  mergedBaseTip: baseTip || verifiedFeatureTip,
10932
11239
  status: "verified",
10933
11240
  verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
10934
- verification
11241
+ verification,
11242
+ featureVerification,
11243
+ postMergeVerification
10935
11244
  };
10936
11245
  await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
10937
11246
  return {
@@ -10946,6 +11255,15 @@ async function runLocalMerge(featureName, options) {
10946
11255
  verification
10947
11256
  };
10948
11257
  }
11258
+ function rollbackLocalIntegration(context, originalBaseTip) {
11259
+ gitRun(context.projectRoot, ["reset", "--hard", originalBaseTip]);
11260
+ if (context.completionStrategy === "local-squash") {
11261
+ gitRun(context.projectRoot, ["update-ref", "-d", context.evidenceRef]);
11262
+ }
11263
+ if (!context.managedFeatureWorktree) {
11264
+ gitRun(context.projectRoot, ["checkout", context.featureBranch]);
11265
+ }
11266
+ }
10949
11267
  async function runLocalCleanup(featureName, options) {
10950
11268
  const cwd = process.cwd();
10951
11269
  const stage = await collectWorkflowStage(cwd, featureName, options.component);