lee-spec-kit 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/dist/index.js +317 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/en/common/README.md +3 -1
- package/templates/en/common/agents/agents.md +1 -1
- package/templates/en/common/features/README.md +5 -1
- package/templates/en/common/features/feature-base/tasks.md +3 -3
- package/templates/ko/common/README.md +3 -1
- package/templates/ko/common/agents/agents.md +1 -1
- package/templates/ko/common/features/README.md +5 -1
- package/templates/ko/common/features/feature-base/tasks.md +3 -3
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
|
|
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
|
|
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
|
|
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,7 +7721,7 @@ 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 &&
|
|
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;
|
|
7699
7727
|
return {
|
|
@@ -7703,12 +7731,13 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7703
7731
|
baseBranch,
|
|
7704
7732
|
featureBranch,
|
|
7705
7733
|
featureTip,
|
|
7734
|
+
featureTree,
|
|
7706
7735
|
baseTip,
|
|
7707
7736
|
currentBranch,
|
|
7708
7737
|
state,
|
|
7709
7738
|
completionStrategy,
|
|
7710
7739
|
baseContainsFeature,
|
|
7711
|
-
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete :
|
|
7740
|
+
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : !!featureTip && baseTip === featureTip,
|
|
7712
7741
|
evidenceRef,
|
|
7713
7742
|
squashCommitMatchesSource,
|
|
7714
7743
|
squashEvidencePresent,
|
|
@@ -7717,7 +7746,10 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7717
7746
|
docsClean: isClean(feature.git.docsGitCwd),
|
|
7718
7747
|
managedFeatureWorktree,
|
|
7719
7748
|
featureBranchExists: !!featureBranchTip,
|
|
7720
|
-
|
|
7749
|
+
featureChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow.featureChecks) : normalizeWorkflowChecks(config.workflow?.postMergeChecks),
|
|
7750
|
+
// Compatibility: pre-0.9.2 postMergeChecks are treated as Feature checks.
|
|
7751
|
+
// Projects can opt into true post-integration checks by defining featureChecks.
|
|
7752
|
+
postMergeChecks: Array.isArray(config.workflow?.featureChecks) ? normalizeWorkflowChecks(config.workflow?.postMergeChecks) : [],
|
|
7721
7753
|
deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
|
|
7722
7754
|
};
|
|
7723
7755
|
}
|
|
@@ -7742,20 +7774,69 @@ async function readLocalIntegrationState(projectRoot, feature) {
|
|
|
7742
7774
|
function localCleanupComplete(context) {
|
|
7743
7775
|
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
7776
|
}
|
|
7745
|
-
function
|
|
7777
|
+
function runLocalChecks(cwd, checks, targetSha, logDir, phase) {
|
|
7746
7778
|
const results = [];
|
|
7779
|
+
fs.ensureDirSync(logDir);
|
|
7747
7780
|
for (const check of checks) {
|
|
7748
7781
|
const args = Array.isArray(check.args) ? check.args.map(String) : [];
|
|
7749
|
-
const
|
|
7782
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
7783
|
+
const result = runProcess(check.command, args, cwd);
|
|
7784
|
+
const finishedAt = /* @__PURE__ */ new Date();
|
|
7785
|
+
const runtime = {
|
|
7786
|
+
node: process.version,
|
|
7787
|
+
platform: process.platform,
|
|
7788
|
+
arch: process.arch,
|
|
7789
|
+
path: process.env.PATH || ""
|
|
7790
|
+
};
|
|
7791
|
+
const index = results.length + 1;
|
|
7792
|
+
const logPath = path8.join(logDir, `${phase}-${String(index).padStart(2, "0")}.log`);
|
|
7793
|
+
const log = [
|
|
7794
|
+
`phase: ${phase}`,
|
|
7795
|
+
`targetSha: ${targetSha}`,
|
|
7796
|
+
`cwd: ${cwd}`,
|
|
7797
|
+
`command: ${JSON.stringify([check.command, ...args])}`,
|
|
7798
|
+
`startedAt: ${startedAt.toISOString()}`,
|
|
7799
|
+
`finishedAt: ${finishedAt.toISOString()}`,
|
|
7800
|
+
`exitCode: ${result.code}`,
|
|
7801
|
+
`runtime: ${JSON.stringify(runtime)}`,
|
|
7802
|
+
"",
|
|
7803
|
+
"--- stdout ---",
|
|
7804
|
+
result.stdout,
|
|
7805
|
+
"--- stderr ---",
|
|
7806
|
+
result.stderr
|
|
7807
|
+
].join("\n");
|
|
7808
|
+
fs.writeFileSync(logPath, log, { encoding: "utf-8", mode: 384 });
|
|
7750
7809
|
results.push({
|
|
7751
7810
|
command: check.command,
|
|
7752
7811
|
args,
|
|
7753
|
-
|
|
7812
|
+
cwd,
|
|
7813
|
+
targetSha,
|
|
7814
|
+
startedAt: startedAt.toISOString(),
|
|
7815
|
+
finishedAt: finishedAt.toISOString(),
|
|
7816
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
7817
|
+
exitCode: result.code,
|
|
7818
|
+
runtime,
|
|
7819
|
+
...preview(result.stdout) ? { stdoutPreview: preview(result.stdout) } : {},
|
|
7820
|
+
...preview(result.stderr) ? { stderrPreview: preview(result.stderr) } : {},
|
|
7821
|
+
logPath
|
|
7754
7822
|
});
|
|
7755
7823
|
if (result.code !== 0) break;
|
|
7756
7824
|
}
|
|
7757
7825
|
return results;
|
|
7758
7826
|
}
|
|
7827
|
+
function resolveLocalIntegrationLogDir(projectRoot, feature, targetSha) {
|
|
7828
|
+
return path8.join(
|
|
7829
|
+
path8.dirname(resolveStatePath(projectRoot, feature)),
|
|
7830
|
+
"logs",
|
|
7831
|
+
resolveFeatureStateKey(feature),
|
|
7832
|
+
targetSha.slice(0, 12)
|
|
7833
|
+
);
|
|
7834
|
+
}
|
|
7835
|
+
function preview(value) {
|
|
7836
|
+
const trimmed = value.trim();
|
|
7837
|
+
return trimmed.length > 2e3 ? `${trimmed.slice(0, 2e3)}
|
|
7838
|
+
\u2026[truncated]` : trimmed;
|
|
7839
|
+
}
|
|
7759
7840
|
function gitRun(cwd, args) {
|
|
7760
7841
|
return runProcess("git", args, cwd);
|
|
7761
7842
|
}
|
|
@@ -7847,7 +7928,7 @@ function resolveRegisteredBranchWorktree(projectRoot, branch) {
|
|
|
7847
7928
|
}
|
|
7848
7929
|
return null;
|
|
7849
7930
|
}
|
|
7850
|
-
function
|
|
7931
|
+
function normalizeWorkflowChecks(value) {
|
|
7851
7932
|
if (!Array.isArray(value)) return [];
|
|
7852
7933
|
return value.filter(
|
|
7853
7934
|
(entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
|
|
@@ -7866,6 +7947,11 @@ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
|
|
|
7866
7947
|
var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
|
|
7867
7948
|
var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
|
|
7868
7949
|
var PRE_PR_DECISION_LABELS = ["Pre-PR Decision", "PR \uC804 \uB9AC\uBDF0 Decision"];
|
|
7950
|
+
var COMPLETION_MARKERS = {
|
|
7951
|
+
allTasks: "lee-spec-kit:completion:all-tasks",
|
|
7952
|
+
tests: "lee-spec-kit:completion:tests",
|
|
7953
|
+
finalOutcome: "lee-spec-kit:completion:final-outcome"
|
|
7954
|
+
};
|
|
7869
7955
|
function resolveWorkflowRequirements(config) {
|
|
7870
7956
|
const workflow = config.workflow || {};
|
|
7871
7957
|
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
@@ -7920,6 +8006,16 @@ function withoutFencedCodeBlocks(content) {
|
|
|
7920
8006
|
}
|
|
7921
8007
|
return lines;
|
|
7922
8008
|
}
|
|
8009
|
+
function parseCompletionCheckbox(lines, marker, legacyPattern) {
|
|
8010
|
+
const markerToken = `<!-- ${marker} -->`;
|
|
8011
|
+
const markedLines = lines.filter((line) => line.includes(markerToken));
|
|
8012
|
+
if (markedLines.length > 0) {
|
|
8013
|
+
return markedLines.length === 1 && parseMarkdownCheckbox(markedLines[0]) === true;
|
|
8014
|
+
}
|
|
8015
|
+
return lines.some(
|
|
8016
|
+
(line) => legacyPattern.test(line) && parseMarkdownCheckbox(line) === true
|
|
8017
|
+
);
|
|
8018
|
+
}
|
|
7923
8019
|
function parseTasksDoc(content) {
|
|
7924
8020
|
const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
|
|
7925
8021
|
const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
|
|
@@ -7940,16 +8036,20 @@ function parseTasksDoc(content) {
|
|
|
7940
8036
|
title: match[2].trim()
|
|
7941
8037
|
});
|
|
7942
8038
|
}
|
|
7943
|
-
const allTasksChecked =
|
|
7944
|
-
|
|
8039
|
+
const allTasksChecked = parseCompletionCheckbox(
|
|
8040
|
+
nonCodeLines,
|
|
8041
|
+
COMPLETION_MARKERS.allTasks,
|
|
8042
|
+
/(All tasks are|모든 태스크가)/i
|
|
7945
8043
|
);
|
|
7946
|
-
const testsChecked =
|
|
7947
|
-
|
|
8044
|
+
const testsChecked = parseCompletionCheckbox(
|
|
8045
|
+
nonCodeLines,
|
|
8046
|
+
COMPLETION_MARKERS.tests,
|
|
8047
|
+
/(Tests executed and passing|테스트 실행 및 통과)/i
|
|
7948
8048
|
);
|
|
7949
|
-
const finalOutcomeChecked =
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
8049
|
+
const finalOutcomeChecked = parseCompletionCheckbox(
|
|
8050
|
+
nonCodeLines,
|
|
8051
|
+
COMPLETION_MARKERS.finalOutcome,
|
|
8052
|
+
/(Final outcome shared and any required user confirmation recorded|Final user approval|최종 결과를 공유했고, 필요한 사용자 확인을 문서화된 workflow checkpoint 기준으로 기록함)/i
|
|
7953
8053
|
);
|
|
7954
8054
|
const prStatus = (() => {
|
|
7955
8055
|
const value = (extractFieldValue2(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
|
|
@@ -9129,8 +9229,49 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9129
9229
|
}
|
|
9130
9230
|
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
|
|
9131
9231
|
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9232
|
+
const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
|
|
9132
9233
|
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9133
9234
|
if (!localState.integrationComplete) {
|
|
9235
|
+
const featureVerified = !!localState.state && ["feature_verified", "merged", "verified", "cleaned"].includes(
|
|
9236
|
+
localState.state.status
|
|
9237
|
+
) && localState.state.verifiedFeatureTip === localState.featureTip && localState.state.verifiedFeatureTree === localState.featureTree;
|
|
9238
|
+
const featureVerificationFailed = localState.state?.status === "feature_failed" && localState.state.featureTip === localState.featureTip;
|
|
9239
|
+
if (featureVerificationFailed) {
|
|
9240
|
+
return {
|
|
9241
|
+
status: "ok",
|
|
9242
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9243
|
+
docsDir: config.docsDir,
|
|
9244
|
+
featureRef: buildFeatureRef(feature),
|
|
9245
|
+
stage: "feature_remediation",
|
|
9246
|
+
nextAction: buildAction(
|
|
9247
|
+
"feature_remediation",
|
|
9248
|
+
`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.`,
|
|
9249
|
+
false,
|
|
9250
|
+
localVerifyCommand
|
|
9251
|
+
),
|
|
9252
|
+
approvalRequired: false,
|
|
9253
|
+
implementationAllowed: true,
|
|
9254
|
+
blockedReasonCode: "FEATURE_REMEDIATION_REQUIRED"
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9257
|
+
if (!featureVerified) {
|
|
9258
|
+
return {
|
|
9259
|
+
status: "ok",
|
|
9260
|
+
reasonCode: "WORKFLOW_STAGE_RESOLVED",
|
|
9261
|
+
docsDir: config.docsDir,
|
|
9262
|
+
featureRef: buildFeatureRef(feature),
|
|
9263
|
+
stage: "feature_verify",
|
|
9264
|
+
nextAction: buildAction(
|
|
9265
|
+
"feature_verify",
|
|
9266
|
+
`Run the configured Feature checks in ${localState.featureBranch} before integration and bind the result to its exact commit and tree.`,
|
|
9267
|
+
false,
|
|
9268
|
+
localVerifyCommand
|
|
9269
|
+
),
|
|
9270
|
+
approvalRequired: false,
|
|
9271
|
+
implementationAllowed: false,
|
|
9272
|
+
blockedReasonCode: "FEATURE_VERIFICATION_REQUIRED"
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9134
9275
|
const approvalRequired = resolveActionApprovalRequired(
|
|
9135
9276
|
config,
|
|
9136
9277
|
"local_merge",
|
|
@@ -10711,6 +10852,14 @@ async function collectDocsAudit(cwd) {
|
|
|
10711
10852
|
// src/commands/local.ts
|
|
10712
10853
|
function localCommand(program2) {
|
|
10713
10854
|
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10855
|
+
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(
|
|
10856
|
+
async (featureName, options) => {
|
|
10857
|
+
await runLocalAction(
|
|
10858
|
+
options,
|
|
10859
|
+
() => runLocalVerify(featureName, options)
|
|
10860
|
+
);
|
|
10861
|
+
}
|
|
10862
|
+
);
|
|
10714
10863
|
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
10864
|
"--confirm <token>",
|
|
10716
10865
|
"Approval token when local_merge requires approval"
|
|
@@ -10731,6 +10880,111 @@ function localCommand(program2) {
|
|
|
10731
10880
|
}
|
|
10732
10881
|
);
|
|
10733
10882
|
}
|
|
10883
|
+
async function runLocalVerify(featureName, options) {
|
|
10884
|
+
const cwd = process.cwd();
|
|
10885
|
+
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
10886
|
+
if (stage.status !== "ok" || !stage.nextAction || !["feature_verify", "feature_remediation"].includes(stage.nextAction.category)) {
|
|
10887
|
+
return blocked(
|
|
10888
|
+
"LOCAL_FEATURE_VERIFY_STAGE_REQUIRED",
|
|
10889
|
+
stage.featureRef,
|
|
10890
|
+
"The active workflow is not at feature_verify or feature_remediation."
|
|
10891
|
+
);
|
|
10892
|
+
}
|
|
10893
|
+
const selection = await resolveFeatureSelection(
|
|
10894
|
+
cwd,
|
|
10895
|
+
featureName,
|
|
10896
|
+
options.component
|
|
10897
|
+
);
|
|
10898
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10899
|
+
return blocked("FEATURE_SELECTION_REQUIRED", null);
|
|
10900
|
+
}
|
|
10901
|
+
const config = await getConfig(cwd);
|
|
10902
|
+
if (!config || config.workflow?.mode !== "local") {
|
|
10903
|
+
return blocked("LOCAL_WORKFLOW_REQUIRED", selection.matchedFeature.folderName);
|
|
10904
|
+
}
|
|
10905
|
+
const feature = selection.matchedFeature;
|
|
10906
|
+
const context = await resolveLocalIntegrationContext(config, feature);
|
|
10907
|
+
if (!context.featureTip || !context.featureTree) {
|
|
10908
|
+
return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
|
|
10909
|
+
}
|
|
10910
|
+
if (!context.featureWorktreeClean || !context.docsClean) {
|
|
10911
|
+
return blocked(
|
|
10912
|
+
"LOCAL_FEATURE_VERIFY_DIRTY_WORKTREE",
|
|
10913
|
+
feature.folderName,
|
|
10914
|
+
"Feature worktree and docs repo must be clean before verification."
|
|
10915
|
+
);
|
|
10916
|
+
}
|
|
10917
|
+
const targetTip = context.featureTip;
|
|
10918
|
+
const targetTree = context.featureTree;
|
|
10919
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10920
|
+
const logDir = resolveLocalIntegrationLogDir(
|
|
10921
|
+
context.projectRoot,
|
|
10922
|
+
feature,
|
|
10923
|
+
targetTip
|
|
10924
|
+
);
|
|
10925
|
+
const verification = runLocalChecks(
|
|
10926
|
+
context.featureWorktree,
|
|
10927
|
+
context.featureChecks,
|
|
10928
|
+
targetTip,
|
|
10929
|
+
logDir,
|
|
10930
|
+
"feature"
|
|
10931
|
+
);
|
|
10932
|
+
const refreshed = await resolveLocalIntegrationContext(config, feature);
|
|
10933
|
+
const changedDuringVerification = refreshed.featureTip !== targetTip || refreshed.featureTree !== targetTree || !refreshed.featureWorktreeClean;
|
|
10934
|
+
const failed = verification.some((entry) => entry.exitCode !== 0);
|
|
10935
|
+
const baseState = {
|
|
10936
|
+
version: 1,
|
|
10937
|
+
featureRef: feature.folderName,
|
|
10938
|
+
component: feature.type,
|
|
10939
|
+
baseBranch: context.baseBranch,
|
|
10940
|
+
featureBranch: context.featureBranch,
|
|
10941
|
+
featureTip: targetTip,
|
|
10942
|
+
mergedBaseTip: context.baseTip || targetTip,
|
|
10943
|
+
strategy: context.completionStrategy,
|
|
10944
|
+
status: failed || changedDuringVerification ? "feature_failed" : "feature_verified",
|
|
10945
|
+
mergedAt: context.state?.mergedAt || now,
|
|
10946
|
+
featureVerifiedAt: failed || changedDuringVerification ? null : now,
|
|
10947
|
+
verifiedAt: null,
|
|
10948
|
+
cleanedAt: null,
|
|
10949
|
+
...failed || changedDuringVerification ? {} : {
|
|
10950
|
+
verifiedFeatureTip: targetTip,
|
|
10951
|
+
verifiedFeatureTree: targetTree
|
|
10952
|
+
},
|
|
10953
|
+
featureVerification: verification,
|
|
10954
|
+
postMergeVerification: [],
|
|
10955
|
+
verification
|
|
10956
|
+
};
|
|
10957
|
+
await writeLocalIntegrationState(context.projectRoot, feature, baseState);
|
|
10958
|
+
if (changedDuringVerification) {
|
|
10959
|
+
return {
|
|
10960
|
+
...blocked(
|
|
10961
|
+
"LOCAL_FEATURE_CHANGED_DURING_VERIFICATION",
|
|
10962
|
+
feature.folderName,
|
|
10963
|
+
"The Feature tip, tree, or worktree changed while checks were running."
|
|
10964
|
+
),
|
|
10965
|
+
featureTip: refreshed.featureTip,
|
|
10966
|
+
verification
|
|
10967
|
+
};
|
|
10968
|
+
}
|
|
10969
|
+
if (failed) {
|
|
10970
|
+
return {
|
|
10971
|
+
...blocked("LOCAL_FEATURE_CHECK_FAILED", feature.folderName),
|
|
10972
|
+
featureTip: targetTip,
|
|
10973
|
+
verification
|
|
10974
|
+
};
|
|
10975
|
+
}
|
|
10976
|
+
return {
|
|
10977
|
+
status: "ok",
|
|
10978
|
+
reasonCode: "LOCAL_FEATURE_VERIFIED",
|
|
10979
|
+
featureRef: feature.folderName,
|
|
10980
|
+
baseBranch: context.baseBranch,
|
|
10981
|
+
featureBranch: context.featureBranch,
|
|
10982
|
+
featureTip: targetTip,
|
|
10983
|
+
baseTip: context.baseTip,
|
|
10984
|
+
completionStrategy: context.completionStrategy,
|
|
10985
|
+
verification
|
|
10986
|
+
};
|
|
10987
|
+
}
|
|
10734
10988
|
async function runLocalAction(options, action) {
|
|
10735
10989
|
try {
|
|
10736
10990
|
const payload = await action();
|
|
@@ -10802,6 +11056,15 @@ async function runLocalMerge(featureName, options) {
|
|
|
10802
11056
|
"Both the configured base branch and Feature tip must exist."
|
|
10803
11057
|
);
|
|
10804
11058
|
}
|
|
11059
|
+
if (!context.integrationComplete && (!context.state || !["feature_verified", "merged", "verified", "cleaned"].includes(
|
|
11060
|
+
context.state.status
|
|
11061
|
+
) || context.state.verifiedFeatureTip !== context.featureTip || context.state.verifiedFeatureTree !== context.featureTree)) {
|
|
11062
|
+
return blocked(
|
|
11063
|
+
"LOCAL_FEATURE_VERIFICATION_REQUIRED",
|
|
11064
|
+
feature.folderName,
|
|
11065
|
+
"Verify the exact Feature tip before local integration."
|
|
11066
|
+
);
|
|
11067
|
+
}
|
|
10805
11068
|
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10806
11069
|
return blocked(
|
|
10807
11070
|
"LOCAL_MERGE_DIRTY_WORKTREE",
|
|
@@ -10810,6 +11073,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10810
11073
|
);
|
|
10811
11074
|
}
|
|
10812
11075
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11076
|
+
const originalBaseTip = context.baseTip;
|
|
10813
11077
|
const previousStateStrategy = context.state?.strategy || "local-ff";
|
|
10814
11078
|
let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
|
|
10815
11079
|
version: 1,
|
|
@@ -10882,6 +11146,7 @@ async function runLocalMerge(featureName, options) {
|
|
|
10882
11146
|
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
10883
11147
|
strategy: context.completionStrategy,
|
|
10884
11148
|
integratedCommit: mergedBaseTip || context.featureTip,
|
|
11149
|
+
originalBaseTip,
|
|
10885
11150
|
...integratedTree ? { integratedTree } : {},
|
|
10886
11151
|
...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
|
|
10887
11152
|
status: "merged",
|
|
@@ -10896,16 +11161,36 @@ async function runLocalMerge(featureName, options) {
|
|
|
10896
11161
|
if (!context.integrationComplete) {
|
|
10897
11162
|
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10898
11163
|
}
|
|
10899
|
-
const
|
|
11164
|
+
const integratedTip = resolveRef(
|
|
11165
|
+
context.projectRoot,
|
|
11166
|
+
`refs/heads/${context.baseBranch}`
|
|
11167
|
+
);
|
|
11168
|
+
const verificationTarget = integratedTip || state.featureTip;
|
|
11169
|
+
const postMergeVerification = runLocalChecks(
|
|
10900
11170
|
context.projectRoot,
|
|
10901
|
-
context.postMergeChecks
|
|
11171
|
+
context.postMergeChecks,
|
|
11172
|
+
verificationTarget,
|
|
11173
|
+
resolveLocalIntegrationLogDir(
|
|
11174
|
+
context.projectRoot,
|
|
11175
|
+
feature,
|
|
11176
|
+
verificationTarget
|
|
11177
|
+
),
|
|
11178
|
+
"post-merge"
|
|
10902
11179
|
);
|
|
10903
|
-
const
|
|
11180
|
+
const featureVerification = state.featureVerification || context.state?.featureVerification || [];
|
|
11181
|
+
const verification = [...featureVerification, ...postMergeVerification];
|
|
11182
|
+
const failedCheck = postMergeVerification.find((entry) => entry.exitCode !== 0);
|
|
10904
11183
|
if (failedCheck) {
|
|
11184
|
+
rollbackLocalIntegration(context, originalBaseTip);
|
|
10905
11185
|
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10906
11186
|
...state,
|
|
10907
|
-
status: "
|
|
11187
|
+
status: "feature_failed",
|
|
11188
|
+
mergedBaseTip: originalBaseTip,
|
|
11189
|
+
integratedCommit: void 0,
|
|
11190
|
+
integratedTree: void 0,
|
|
10908
11191
|
verification,
|
|
11192
|
+
featureVerification,
|
|
11193
|
+
postMergeVerification,
|
|
10909
11194
|
verifiedAt: null
|
|
10910
11195
|
});
|
|
10911
11196
|
return {
|
|
@@ -10931,7 +11216,9 @@ async function runLocalMerge(featureName, options) {
|
|
|
10931
11216
|
mergedBaseTip: baseTip || verifiedFeatureTip,
|
|
10932
11217
|
status: "verified",
|
|
10933
11218
|
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10934
|
-
verification
|
|
11219
|
+
verification,
|
|
11220
|
+
featureVerification,
|
|
11221
|
+
postMergeVerification
|
|
10935
11222
|
};
|
|
10936
11223
|
await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
|
|
10937
11224
|
return {
|
|
@@ -10946,6 +11233,15 @@ async function runLocalMerge(featureName, options) {
|
|
|
10946
11233
|
verification
|
|
10947
11234
|
};
|
|
10948
11235
|
}
|
|
11236
|
+
function rollbackLocalIntegration(context, originalBaseTip) {
|
|
11237
|
+
gitRun(context.projectRoot, ["reset", "--hard", originalBaseTip]);
|
|
11238
|
+
if (context.completionStrategy === "local-squash") {
|
|
11239
|
+
gitRun(context.projectRoot, ["update-ref", "-d", context.evidenceRef]);
|
|
11240
|
+
}
|
|
11241
|
+
if (!context.managedFeatureWorktree) {
|
|
11242
|
+
gitRun(context.projectRoot, ["checkout", context.featureBranch]);
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
10949
11245
|
async function runLocalCleanup(featureName, options) {
|
|
10950
11246
|
const cwd = process.cwd();
|
|
10951
11247
|
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|