lee-spec-kit 0.9.0 → 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 +3 -2
- package/dist/bootstrap-Q77MTW3Q.js +0 -0
- package/dist/chunk-3AFCPGGS.js +0 -0
- package/dist/chunk-7V7RMGEU.js +0 -0
- package/dist/chunk-GR7JQBWF.js +0 -0
- package/dist/hooks-373Z6JG2.js +0 -0
- package/dist/index.js +600 -89
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
- package/templates/en/common/README.md +11 -9
- package/templates/en/common/agents/agents.md +9 -3
- package/templates/en/common/agents/git-workflow.md +27 -8
- package/templates/en/common/agents/skills/split-feature.md +2 -1
- package/templates/en/common/agents/ui-ux-design.md +128 -0
- package/templates/en/common/designs/README.md +39 -1
- package/templates/en/common/features/README.md +6 -2
- package/templates/en/common/features/feature-base/decisions.md +3 -1
- package/templates/en/common/features/feature-base/spec.md +3 -0
- package/templates/en/common/features/feature-base/tasks.md +4 -3
- package/templates/ko/common/README.md +11 -9
- package/templates/ko/common/agents/agents.md +9 -3
- package/templates/ko/common/agents/git-workflow.md +27 -8
- package/templates/ko/common/agents/skills/split-feature.md +4 -3
- package/templates/ko/common/agents/ui-ux-design.md +128 -0
- package/templates/ko/common/designs/README.md +39 -1
- package/templates/ko/common/features/README.md +6 -2
- package/templates/ko/common/features/feature-base/decisions.md +3 -1
- package/templates/ko/common/features/feature-base/spec.md +3 -0
- package/templates/ko/common/features/feature-base/tasks.md +4 -3
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { spawn, execFileSync, spawnSync } from 'child_process';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import crypto, { createHash } from 'crypto';
|
|
14
14
|
import fs21 from 'fs';
|
|
15
|
+
import fs26 from 'fs/promises';
|
|
15
16
|
|
|
16
17
|
async function walkFiles(fsAdapter, rootDir, options = {}) {
|
|
17
18
|
const out = [];
|
|
@@ -1231,7 +1232,8 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1231
1232
|
"agents.md",
|
|
1232
1233
|
"git-workflow.md",
|
|
1233
1234
|
"issue-template.md",
|
|
1234
|
-
"pr-template.md"
|
|
1235
|
+
"pr-template.md",
|
|
1236
|
+
"ui-ux-design.md"
|
|
1235
1237
|
];
|
|
1236
1238
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1237
1239
|
var ENGINE_MANAGED_FEATURE_PATH = path8.join(
|
|
@@ -1306,8 +1308,8 @@ Before taking the next workflow step:
|
|
|
1306
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
|
|
1307
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
|
|
1308
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
|
|
1309
|
-
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
|
|
1310
|
-
16. In a \`local-ff\` workflow,
|
|
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
|
+
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
|
|
1311
1313
|
|
|
1312
1314
|
Approval and remote actions:
|
|
1313
1315
|
|
|
@@ -1315,14 +1317,20 @@ Approval and remote actions:
|
|
|
1315
1317
|
- If \`workflow-stage --json\` reports \`approvalRequired === true\`, stop at that boundary and ask the user before proceeding
|
|
1316
1318
|
- If \`workflow-stage --json\` returns labeled \`actionOptions\` at any approval boundary, keep the same option labels and exact \`reply\` tokens in the user prompt and do not improvise different reply formats
|
|
1317
1319
|
- If \`workflow-stage --json\` reports \`nextAction.category === "task_commit"\`, make the docs commit and project commit for the just-finished task before starting the next task or moving to the next stage
|
|
1318
|
-
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\` when
|
|
1320
|
+
- Before \`git commit\`, prefer \`npx lee-spec-kit commit-audit --json\`; Feature-scoped commits use \`#123\` when an Issue is linked and the stable Feature ID such as \`F027\` for issue-less local workflows
|
|
1319
1321
|
- Before remote GitHub actions, share the plan or artifact being sent
|
|
1320
1322
|
- Respect repo policy from docs and config first; hooks only enforce guardrails and continuation checks
|
|
1321
1323
|
|
|
1322
1324
|
Validation:
|
|
1323
1325
|
|
|
1324
|
-
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation
|
|
1326
|
+
- Prefer \`npx lee-spec-kit commit-audit --json\` for commit-time staged docs path validation and canonical commit-subject validation
|
|
1325
1327
|
- 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
|
|
1328
|
+
|
|
1329
|
+
Optional UI/UX design policy:
|
|
1330
|
+
|
|
1331
|
+
- Only when the user request explicitly mentions a design system, UI/visual redesign, design consistency, shared UI/component-library consolidation, branding/theme/token redesign, or implementation from Figma/design images, read and apply \`npx lee-spec-kit docs get ui-ux-design --json\`
|
|
1332
|
+
- Do not apply that policy merely because the target is web/frontend, to a non-UI/backend Feature, or to a simple bug fix unrelated to durable design rules
|
|
1333
|
+
- Treat it as optional guidance, not a \`requiredDocs\` entry or workflow approval gate
|
|
1326
1334
|
`;
|
|
1327
1335
|
function renderManagedSegment(lang, docsRepo) {
|
|
1328
1336
|
return `${LEE_SPEC_KIT_AGENTS_BEGIN}
|
|
@@ -1887,7 +1895,8 @@ async function getConfig(cwd) {
|
|
|
1887
1895
|
// src/config/types.ts
|
|
1888
1896
|
var DEFAULT_APPROVAL_REQUIRE_CHECK_CATEGORIES = [
|
|
1889
1897
|
"spec_approve",
|
|
1890
|
-
"implementation_approve"
|
|
1898
|
+
"implementation_approve",
|
|
1899
|
+
"local_merge"
|
|
1891
1900
|
];
|
|
1892
1901
|
var PRE_PR_REVIEW_REASONING_EFFORTS = [
|
|
1893
1902
|
"low",
|
|
@@ -2534,6 +2543,7 @@ async function runInit(options) {
|
|
|
2534
2543
|
baseBranch: resolveInitialBaseBranch(cwd, docsRepo, projectRoot),
|
|
2535
2544
|
completionStrategy: "local-ff",
|
|
2536
2545
|
deleteFeatureBranchAfterMerge: true,
|
|
2546
|
+
featureChecks: [],
|
|
2537
2547
|
postMergeChecks: []
|
|
2538
2548
|
} : {},
|
|
2539
2549
|
prePrReview: {
|
|
@@ -3481,6 +3491,11 @@ function isLegacyGeneratedApprovalConfig(approval) {
|
|
|
3481
3491
|
];
|
|
3482
3492
|
return !overrideKeys.some((key) => hasOwnKey2(approval, key));
|
|
3483
3493
|
}
|
|
3494
|
+
function isPreviousDefaultApprovalConfig(approval) {
|
|
3495
|
+
const keys = Object.keys(approval).sort();
|
|
3496
|
+
const categories = Array.isArray(approval.requireCheckCategories) ? approval.requireCheckCategories : [];
|
|
3497
|
+
return JSON.stringify(keys) === JSON.stringify(["default", "mode", "requireCheckCategories"]) && approval.mode === "category" && approval.default === "skip" && categories.length === 2 && categories[0] === "spec_approve" && categories[1] === "implementation_approve";
|
|
3498
|
+
}
|
|
3484
3499
|
function updateCommand(program2) {
|
|
3485
3500
|
program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--agents-md", "Sync project-scoped AGENTS.md entrypoint").option(
|
|
3486
3501
|
"-f, --force",
|
|
@@ -3720,7 +3735,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3720
3735
|
"none",
|
|
3721
3736
|
"workflow.completionStrategy"
|
|
3722
3737
|
);
|
|
3723
|
-
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "none") {
|
|
3738
|
+
if (workflow.completionStrategy !== "local-ff" && workflow.completionStrategy !== "local-squash" && workflow.completionStrategy !== "none") {
|
|
3724
3739
|
workflow.completionStrategy = "none";
|
|
3725
3740
|
changedPaths.push("workflow.completionStrategy");
|
|
3726
3741
|
}
|
|
@@ -3734,6 +3749,33 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3734
3749
|
workflow.deleteFeatureBranchAfterMerge = true;
|
|
3735
3750
|
changedPaths.push("workflow.deleteFeatureBranchAfterMerge");
|
|
3736
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
|
+
}
|
|
3737
3779
|
setIfMissing(workflow, "postMergeChecks", [], "workflow.postMergeChecks");
|
|
3738
3780
|
if (!Array.isArray(workflow.postMergeChecks)) {
|
|
3739
3781
|
workflow.postMergeChecks = [];
|
|
@@ -3822,7 +3864,7 @@ async function backfillMissingConfigDefaults(cwd, docsDir) {
|
|
|
3822
3864
|
const migration = migrateLegacyApprovalSettings(approval);
|
|
3823
3865
|
raw.approval = migration.approval;
|
|
3824
3866
|
changedPaths.push(...migration.changedPaths);
|
|
3825
|
-
if (isPlainObject(raw.approval) && isLegacyGeneratedApprovalConfig(raw.approval)) {
|
|
3867
|
+
if (isPlainObject(raw.approval) && (isLegacyGeneratedApprovalConfig(raw.approval) || isPreviousDefaultApprovalConfig(raw.approval))) {
|
|
3826
3868
|
raw.approval = createDefaultApprovalConfig();
|
|
3827
3869
|
changedPaths.push("approval");
|
|
3828
3870
|
}
|
|
@@ -6697,6 +6739,14 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
6697
6739
|
id: "split-feature",
|
|
6698
6740
|
title: { ko: "feature \uBD84\uD560 \uAC00\uC774\uB4DC", en: "feature split guide" },
|
|
6699
6741
|
relativePath: (_, lang) => path8.join(lang, "common", "agents", "skills", "split-feature.md")
|
|
6742
|
+
},
|
|
6743
|
+
{
|
|
6744
|
+
id: "ui-ux-design",
|
|
6745
|
+
title: {
|
|
6746
|
+
ko: "\uC120\uD0DD\uC801 UI/UX \uB514\uC790\uC778 \uBB38\uC11C \uC815\uCC45",
|
|
6747
|
+
en: "Optional UI/UX Design Documentation Policy"
|
|
6748
|
+
},
|
|
6749
|
+
relativePath: (_, lang) => path8.join(lang, "common", "agents", "ui-ux-design.md")
|
|
6700
6750
|
}
|
|
6701
6751
|
];
|
|
6702
6752
|
var DOC_FOLLOWUPS = {
|
|
@@ -6717,7 +6767,8 @@ var DOC_FOLLOWUPS = {
|
|
|
6717
6767
|
"execute-task": ["git-workflow", "split-feature"],
|
|
6718
6768
|
"create-issue": ["issue-doc"],
|
|
6719
6769
|
"create-pr": ["pr-doc"],
|
|
6720
|
-
"split-feature": []
|
|
6770
|
+
"split-feature": [],
|
|
6771
|
+
"ui-ux-design": []
|
|
6721
6772
|
};
|
|
6722
6773
|
function getBuiltinDocIds() {
|
|
6723
6774
|
return BUILTIN_DOC_DEFINITIONS.map((doc) => doc.id);
|
|
@@ -6736,6 +6787,7 @@ function normalizeBuiltinDocId(input) {
|
|
|
6736
6787
|
if (normalized === "split-feature") {
|
|
6737
6788
|
return "split-feature";
|
|
6738
6789
|
}
|
|
6790
|
+
if (normalized === "ui-ux-design") return "ui-ux-design";
|
|
6739
6791
|
if (normalized === "agents") return "agents";
|
|
6740
6792
|
return null;
|
|
6741
6793
|
}
|
|
@@ -7197,11 +7249,9 @@ function findPlaceholderDecisionRange(content) {
|
|
|
7197
7249
|
const block = content.slice(match.index, end);
|
|
7198
7250
|
const isPlaceholder = /\{(?:Decision Title|결정 제목)\}/.test(match[0]) || /-\s+\*\*Decision\*\*:\s*(Final choice|최종 선택)/.test(block);
|
|
7199
7251
|
if (!isPlaceholder) return null;
|
|
7200
|
-
const sequence = Number(match[1] || "1");
|
|
7201
7252
|
return {
|
|
7202
7253
|
start: match.index,
|
|
7203
|
-
end
|
|
7204
|
-
sequence: Number.isFinite(sequence) ? sequence : 1
|
|
7254
|
+
end
|
|
7205
7255
|
};
|
|
7206
7256
|
}
|
|
7207
7257
|
function formatOptions(options) {
|
|
@@ -7242,7 +7292,7 @@ async function runDecisionAdd(featureName, options) {
|
|
|
7242
7292
|
const optionItems = (options.option || []).map((value) => value.trim()).filter(Boolean);
|
|
7243
7293
|
const content = await fs.readFile(target.path, "utf-8");
|
|
7244
7294
|
const placeholderRange = findPlaceholderDecisionRange(content);
|
|
7245
|
-
const decisionSequence = placeholderRange
|
|
7295
|
+
const decisionSequence = placeholderRange ? 1 : getNextDecisionSequence(content);
|
|
7246
7296
|
const decisionId = `D${String(decisionSequence).padStart(3, "0")}`;
|
|
7247
7297
|
const recordedAt = localDate();
|
|
7248
7298
|
const block = formatDecisionBlock({
|
|
@@ -7551,6 +7601,44 @@ function integrationsCommand(program2) {
|
|
|
7551
7601
|
registerCodexIntegration(integrations);
|
|
7552
7602
|
registerCodexHooksIntegration(integrations);
|
|
7553
7603
|
}
|
|
7604
|
+
|
|
7605
|
+
// src/utils/commit-conventions.ts
|
|
7606
|
+
var PROJECT_COMMIT_TYPES = [
|
|
7607
|
+
"feat",
|
|
7608
|
+
"fix",
|
|
7609
|
+
"refactor",
|
|
7610
|
+
"test",
|
|
7611
|
+
"style",
|
|
7612
|
+
"chore"
|
|
7613
|
+
];
|
|
7614
|
+
function resolveFeatureCommitScope(input) {
|
|
7615
|
+
const issueNumber = Number(input.issueNumber || 0);
|
|
7616
|
+
if (Number.isInteger(issueNumber) && issueNumber > 0) {
|
|
7617
|
+
return `#${issueNumber}`;
|
|
7618
|
+
}
|
|
7619
|
+
if (input.workflowMode !== "local") {
|
|
7620
|
+
return null;
|
|
7621
|
+
}
|
|
7622
|
+
const featureId = String(input.featureId || "").trim().toUpperCase();
|
|
7623
|
+
return /^F\d{3,}$/.test(featureId) ? featureId : null;
|
|
7624
|
+
}
|
|
7625
|
+
function matchesProjectCommitConvention(message, scope) {
|
|
7626
|
+
return matchesCommitConvention(message, scope, PROJECT_COMMIT_TYPES);
|
|
7627
|
+
}
|
|
7628
|
+
function matchesDocsCommitConvention(message, scope) {
|
|
7629
|
+
return matchesCommitConvention(message, scope, ["docs"]);
|
|
7630
|
+
}
|
|
7631
|
+
function matchesCommitConvention(message, scope, types) {
|
|
7632
|
+
const normalized = String(message || "").trim();
|
|
7633
|
+
const normalizedScope = String(scope || "").trim();
|
|
7634
|
+
if (!normalized || !normalizedScope) return false;
|
|
7635
|
+
const typePattern = types.join("|");
|
|
7636
|
+
const match = normalized.match(
|
|
7637
|
+
new RegExp(`^(${typePattern})\\((#[0-9]+|F[0-9]{3,})\\):\\s+\\S.+$`, "i")
|
|
7638
|
+
);
|
|
7639
|
+
if (!match) return false;
|
|
7640
|
+
return match[2].toLowerCase() === normalizedScope.toLowerCase();
|
|
7641
|
+
}
|
|
7554
7642
|
function isSameOrWithin2(parentDir, candidatePath) {
|
|
7555
7643
|
const relative = path8.relative(parentDir, candidatePath);
|
|
7556
7644
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relative);
|
|
@@ -7591,7 +7679,8 @@ function isPrePrEvidenceSatisfied(check) {
|
|
|
7591
7679
|
}
|
|
7592
7680
|
var BRANCH_LABELS2 = ["Branch", "\uBE0C\uB79C\uCE58"];
|
|
7593
7681
|
function resolveLocalCompletionStrategy(config) {
|
|
7594
|
-
|
|
7682
|
+
const strategy = config.workflow?.completionStrategy;
|
|
7683
|
+
return strategy === "local-ff" || strategy === "local-squash" ? strategy : "none";
|
|
7595
7684
|
}
|
|
7596
7685
|
async function resolveLocalIntegrationContext(config, feature) {
|
|
7597
7686
|
const projectRoot = resolveProjectRoot(config, feature);
|
|
@@ -7611,6 +7700,9 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7611
7700
|
);
|
|
7612
7701
|
}
|
|
7613
7702
|
const state = await readLocalIntegrationState(projectRoot, feature);
|
|
7703
|
+
const configuredStrategy = resolveLocalCompletionStrategy(config);
|
|
7704
|
+
const completionStrategy = configuredStrategy === "local-squash" ? "local-squash" : "local-ff";
|
|
7705
|
+
const evidenceRef = resolveEvidenceRef(feature);
|
|
7614
7706
|
const featureBranchTip = resolveRef(
|
|
7615
7707
|
projectRoot,
|
|
7616
7708
|
`refs/heads/${featureBranch}`
|
|
@@ -7624,6 +7716,14 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7624
7716
|
);
|
|
7625
7717
|
const managedFeatureWorktree = !!registeredWorktree && path8.resolve(registeredWorktree) !== path8.resolve(projectRoot);
|
|
7626
7718
|
const effectiveFeatureWorktree = registeredWorktree || (fs.existsSync(featureWorktree) ? featureWorktree : projectRoot);
|
|
7719
|
+
const baseContainsFeature = !!featureTip && !!baseTip && isAncestor(projectRoot, featureTip, `refs/heads/${baseBranch}`);
|
|
7720
|
+
const integratedCommit = state?.integratedCommit || state?.mergedBaseTip;
|
|
7721
|
+
const evidenceTip = resolveRef(projectRoot, evidenceRef);
|
|
7722
|
+
const featureTree = featureTip ? resolveTree(projectRoot, featureTip) : null;
|
|
7723
|
+
const integratedTree = integratedCommit ? resolveTree(projectRoot, integratedCommit) : null;
|
|
7724
|
+
const squashCommitMatchesSource = completionStrategy === "local-squash" && state?.strategy === "local-squash" && !!featureTip && state.featureTip === featureTip && !!integratedCommit && !!baseTip && integratedCommit === baseTip && !!featureTree && integratedTree === featureTree && state.integratedTree === featureTree;
|
|
7725
|
+
const squashEvidencePresent = !!featureTip && evidenceTip === featureTip;
|
|
7726
|
+
const squashIntegrationComplete = squashCommitMatchesSource && squashEvidencePresent;
|
|
7627
7727
|
return {
|
|
7628
7728
|
projectRoot,
|
|
7629
7729
|
featureWorktree: effectiveFeatureWorktree,
|
|
@@ -7631,16 +7731,25 @@ async function resolveLocalIntegrationContext(config, feature) {
|
|
|
7631
7731
|
baseBranch,
|
|
7632
7732
|
featureBranch,
|
|
7633
7733
|
featureTip,
|
|
7734
|
+
featureTree,
|
|
7634
7735
|
baseTip,
|
|
7635
7736
|
currentBranch,
|
|
7636
7737
|
state,
|
|
7637
|
-
|
|
7738
|
+
completionStrategy,
|
|
7739
|
+
baseContainsFeature,
|
|
7740
|
+
integrationComplete: completionStrategy === "local-squash" ? squashIntegrationComplete : !!featureTip && baseTip === featureTip,
|
|
7741
|
+
evidenceRef,
|
|
7742
|
+
squashCommitMatchesSource,
|
|
7743
|
+
squashEvidencePresent,
|
|
7638
7744
|
projectRootClean: isClean(projectRoot),
|
|
7639
7745
|
featureWorktreeClean: isClean(effectiveFeatureWorktree),
|
|
7640
7746
|
docsClean: isClean(feature.git.docsGitCwd),
|
|
7641
7747
|
managedFeatureWorktree,
|
|
7642
7748
|
featureBranchExists: !!featureBranchTip,
|
|
7643
|
-
|
|
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) : [],
|
|
7644
7753
|
deleteFeatureBranchAfterMerge: config.workflow?.deleteFeatureBranchAfterMerge !== false
|
|
7645
7754
|
};
|
|
7646
7755
|
}
|
|
@@ -7663,22 +7772,71 @@ async function readLocalIntegrationState(projectRoot, feature) {
|
|
|
7663
7772
|
}
|
|
7664
7773
|
}
|
|
7665
7774
|
function localCleanupComplete(context) {
|
|
7666
|
-
return context.
|
|
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;
|
|
7667
7776
|
}
|
|
7668
|
-
function
|
|
7777
|
+
function runLocalChecks(cwd, checks, targetSha, logDir, phase) {
|
|
7669
7778
|
const results = [];
|
|
7779
|
+
fs.ensureDirSync(logDir);
|
|
7670
7780
|
for (const check of checks) {
|
|
7671
7781
|
const args = Array.isArray(check.args) ? check.args.map(String) : [];
|
|
7672
|
-
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 });
|
|
7673
7809
|
results.push({
|
|
7674
7810
|
command: check.command,
|
|
7675
7811
|
args,
|
|
7676
|
-
|
|
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
|
|
7677
7822
|
});
|
|
7678
7823
|
if (result.code !== 0) break;
|
|
7679
7824
|
}
|
|
7680
7825
|
return results;
|
|
7681
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
|
+
}
|
|
7682
7840
|
function gitRun(cwd, args) {
|
|
7683
7841
|
return runProcess("git", args, cwd);
|
|
7684
7842
|
}
|
|
@@ -7688,6 +7846,12 @@ function currentGitBranch(cwd) {
|
|
|
7688
7846
|
function resolveRef(cwd, ref) {
|
|
7689
7847
|
return runGitCapture(["rev-parse", "--verify", ref], cwd) || null;
|
|
7690
7848
|
}
|
|
7849
|
+
function resolveTree(cwd, commit) {
|
|
7850
|
+
return resolveRef(cwd, `${commit}^{tree}`);
|
|
7851
|
+
}
|
|
7852
|
+
function preserveLocalIntegrationEvidence(projectRoot, evidenceRef, featureTip) {
|
|
7853
|
+
return gitRun(projectRoot, ["update-ref", evidenceRef, featureTip]);
|
|
7854
|
+
}
|
|
7691
7855
|
function isAncestor(cwd, ancestor, descendant) {
|
|
7692
7856
|
return runProcess(
|
|
7693
7857
|
"git",
|
|
@@ -7711,7 +7875,7 @@ function resolveProjectRoot(config, feature) {
|
|
|
7711
7875
|
function resolveStatePath(projectRoot, feature) {
|
|
7712
7876
|
const commonDirRaw = runGitCapture(["rev-parse", "--git-common-dir"], projectRoot) || ".git";
|
|
7713
7877
|
const commonDir = path8.isAbsolute(commonDirRaw) ? commonDirRaw : path8.resolve(projectRoot, commonDirRaw);
|
|
7714
|
-
const key =
|
|
7878
|
+
const key = resolveFeatureStateKey(feature);
|
|
7715
7879
|
return path8.join(
|
|
7716
7880
|
commonDir,
|
|
7717
7881
|
"lee-spec-kit",
|
|
@@ -7719,6 +7883,12 @@ function resolveStatePath(projectRoot, feature) {
|
|
|
7719
7883
|
`${key}.json`
|
|
7720
7884
|
);
|
|
7721
7885
|
}
|
|
7886
|
+
function resolveEvidenceRef(feature) {
|
|
7887
|
+
return `refs/lee-spec-kit/integrations/${resolveFeatureStateKey(feature)}`;
|
|
7888
|
+
}
|
|
7889
|
+
function resolveFeatureStateKey(feature) {
|
|
7890
|
+
return crypto.createHash("sha256").update(`${feature.type}:${feature.folderName}`).digest("hex").slice(0, 24);
|
|
7891
|
+
}
|
|
7722
7892
|
async function readTasks(featureDir) {
|
|
7723
7893
|
const tasksPath = path8.join(featureDir, "tasks.md");
|
|
7724
7894
|
return await fs.pathExists(tasksPath) ? fs.readFile(tasksPath, "utf-8") : "";
|
|
@@ -7758,7 +7928,7 @@ function resolveRegisteredBranchWorktree(projectRoot, branch) {
|
|
|
7758
7928
|
}
|
|
7759
7929
|
return null;
|
|
7760
7930
|
}
|
|
7761
|
-
function
|
|
7931
|
+
function normalizeWorkflowChecks(value) {
|
|
7762
7932
|
if (!Array.isArray(value)) return [];
|
|
7763
7933
|
return value.filter(
|
|
7764
7934
|
(entry) => !!entry && typeof entry === "object" && typeof entry.command === "string" && !!entry.command.trim()
|
|
@@ -7777,6 +7947,11 @@ var PR_STATUS_LABELS = ["PR Status", "PR \uC0C1\uD0DC"];
|
|
|
7777
7947
|
var PRE_PR_REVIEW_LABELS = ["Pre-PR Review", "PR \uC804 \uB9AC\uBDF0"];
|
|
7778
7948
|
var PRE_PR_EVIDENCE_LABELS = ["Pre-PR Evidence", "PR \uC804 \uB9AC\uBDF0 Evidence"];
|
|
7779
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
|
+
};
|
|
7780
7955
|
function resolveWorkflowRequirements(config) {
|
|
7781
7956
|
const workflow = config.workflow || {};
|
|
7782
7957
|
const hasCanonicalMode = workflow.mode === "github" || workflow.mode === "local";
|
|
@@ -7831,6 +8006,16 @@ function withoutFencedCodeBlocks(content) {
|
|
|
7831
8006
|
}
|
|
7832
8007
|
return lines;
|
|
7833
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
|
+
}
|
|
7834
8019
|
function parseTasksDoc(content) {
|
|
7835
8020
|
const issueRaw = extractFieldValue2(content, ISSUE_LABELS);
|
|
7836
8021
|
const issueNumberMatch = issueRaw?.match(/^#(\d+)$/);
|
|
@@ -7851,16 +8036,20 @@ function parseTasksDoc(content) {
|
|
|
7851
8036
|
title: match[2].trim()
|
|
7852
8037
|
});
|
|
7853
8038
|
}
|
|
7854
|
-
const allTasksChecked =
|
|
7855
|
-
|
|
8039
|
+
const allTasksChecked = parseCompletionCheckbox(
|
|
8040
|
+
nonCodeLines,
|
|
8041
|
+
COMPLETION_MARKERS.allTasks,
|
|
8042
|
+
/(All tasks are|모든 태스크가)/i
|
|
7856
8043
|
);
|
|
7857
|
-
const testsChecked =
|
|
7858
|
-
|
|
8044
|
+
const testsChecked = parseCompletionCheckbox(
|
|
8045
|
+
nonCodeLines,
|
|
8046
|
+
COMPLETION_MARKERS.tests,
|
|
8047
|
+
/(Tests executed and passing|테스트 실행 및 통과)/i
|
|
7859
8048
|
);
|
|
7860
|
-
const finalOutcomeChecked =
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
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
|
|
7864
8053
|
);
|
|
7865
8054
|
const prStatus = (() => {
|
|
7866
8055
|
const value = (extractFieldValue2(content, PR_STATUS_LABELS) || "").trim().toLowerCase();
|
|
@@ -8116,8 +8305,13 @@ function resolveProjectCommitTopic(feature, tasks) {
|
|
|
8116
8305
|
}
|
|
8117
8306
|
function buildTaskCommitSummary(input) {
|
|
8118
8307
|
const { feature, tasks, effectiveProjectGitCwd, docsDirty, projectDirty, gateFailureReason } = input;
|
|
8119
|
-
const
|
|
8120
|
-
|
|
8308
|
+
const scope = resolveFeatureCommitScope({
|
|
8309
|
+
issueNumber: tasks.issueNumber,
|
|
8310
|
+
featureId: feature.id,
|
|
8311
|
+
workflowMode: tasks.issueNumber ? "github" : "local"
|
|
8312
|
+
}) || feature.id;
|
|
8313
|
+
const docsMessage = `git -C "${feature.git.docsGitCwd}" add "${feature.docs.featurePathFromDocs}" && git -C "${feature.git.docsGitCwd}" commit -m "docs(${scope}): ${feature.folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"`;
|
|
8314
|
+
const projectMessage = `Stage only the files touched by the just-finished task in "${effectiveProjectGitCwd}", then commit with: git -C "${effectiveProjectGitCwd}" commit -m "feat(${scope}): ${resolveProjectCommitTopic(feature, tasks)}"`;
|
|
8121
8315
|
const lines = ["Finish the task-level commit checkpoint before continuing."];
|
|
8122
8316
|
if (gateFailureReason) {
|
|
8123
8317
|
lines.push(`Current gate failure: ${gateFailureReason}`);
|
|
@@ -8987,11 +9181,13 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
8987
9181
|
"implementation_approve",
|
|
8988
9182
|
true
|
|
8989
9183
|
);
|
|
8990
|
-
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config)
|
|
9184
|
+
const localIntegrationApproval = config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none";
|
|
9185
|
+
const separateLocalMergeApproval = localIntegrationApproval && resolveActionApprovalRequired(config, "local_merge", false);
|
|
8991
9186
|
const localBaseBranch = config.workflow?.baseBranch?.trim() || "main";
|
|
9187
|
+
const localIntegrationLabel = resolveLocalCompletionStrategy(config) === "local-squash" ? `squash integration into ${localBaseBranch}` : `fast-forward integration into ${localBaseBranch}`;
|
|
8992
9188
|
const localCleanupSummary = config.workflow?.deleteFeatureBranchAfterMerge === false ? "remove any managed Feature worktree" : "remove any managed Feature worktree and delete the integrated local Feature branch";
|
|
8993
9189
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
8994
|
-
approveSummary:
|
|
9190
|
+
approveSummary: separateLocalMergeApproval ? `Approve the completed implementation and continue to the separate local merge approval before ${localIntegrationLabel}.` : localIntegrationApproval ? `Approve the completed implementation and authorize ${localIntegrationLabel}, post-merge verification, and cleanup that will ${localCleanupSummary}.` : "Approve the completed implementation and continue to the pre-PR or PR preparation stage.",
|
|
8995
9191
|
holdSummary: "Request implementation changes before the workflow continues."
|
|
8996
9192
|
}) : void 0;
|
|
8997
9193
|
return {
|
|
@@ -9002,7 +9198,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9002
9198
|
stage: "implementation_approve",
|
|
9003
9199
|
nextAction: buildAction(
|
|
9004
9200
|
"implementation_approve",
|
|
9005
|
-
localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow:
|
|
9201
|
+
separateLocalMergeApproval ? `Share the completed implementation and get user approval for the implementation itself. Record it in tasks.md; ${localIntegrationLabel} will require a separate local_merge approval.` : localIntegrationApproval ? `Share the completed implementation and get user approval for the remaining local completion flow: ${localIntegrationLabel}, run post-merge checks, then ${localCleanupSummary}. Record that approval in tasks.md.` : "Share the completed implementation, get user approval, and record the completion checkpoint in tasks.md.",
|
|
9006
9202
|
approvalRequired
|
|
9007
9203
|
),
|
|
9008
9204
|
approvalRequired,
|
|
@@ -9031,10 +9227,51 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9031
9227
|
blockedReasonCode: "PRE_PR_REVIEW_NOT_APPROVED"
|
|
9032
9228
|
};
|
|
9033
9229
|
}
|
|
9034
|
-
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config)
|
|
9230
|
+
if (config.workflow?.mode === "local" && resolveLocalCompletionStrategy(config) !== "none") {
|
|
9035
9231
|
const localState = await resolveLocalIntegrationContext(config, feature);
|
|
9232
|
+
const localVerifyCommand = `npx lee-spec-kit local verify ${buildFeatureArgs(feature)} --json`;
|
|
9036
9233
|
const localMergeBaseCommand = `npx lee-spec-kit local merge ${buildFeatureArgs(feature)} --json`;
|
|
9037
|
-
if (!localState.
|
|
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
|
+
}
|
|
9038
9275
|
const approvalRequired = resolveActionApprovalRequired(
|
|
9039
9276
|
config,
|
|
9040
9277
|
"local_merge",
|
|
@@ -9042,7 +9279,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9042
9279
|
);
|
|
9043
9280
|
const command = approvalRequired ? `${localMergeBaseCommand} --confirm OK` : localMergeBaseCommand;
|
|
9044
9281
|
const actionOptions = approvalRequired ? buildApprovalActionOptions({
|
|
9045
|
-
approveSummary: `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9282
|
+
approveSummary: localState.completionStrategy === "local-squash" ? `Squash ${localState.featureBranch} into ${localState.baseBranch}, preserve the source Feature tip, and run the configured post-merge checks.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch} and run the configured post-merge checks.`,
|
|
9046
9283
|
holdSummary: "Keep the completed Feature branch unmerged for now."
|
|
9047
9284
|
}) : void 0;
|
|
9048
9285
|
return {
|
|
@@ -9053,7 +9290,7 @@ Task commit boundary warning: ${describeTaskCommitGateFailure(committedTaskGate)
|
|
|
9053
9290
|
stage: "local_merge",
|
|
9054
9291
|
nextAction: buildAction(
|
|
9055
9292
|
"local_merge",
|
|
9056
|
-
`Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9293
|
+
localState.completionStrategy === "local-squash" ? `Create one squash commit from ${localState.featureBranch} on ${localState.baseBranch} and preserve the source Feature tip as internal integration evidence.` : `Fast-forward ${localState.featureBranch} into ${localState.baseBranch}; do not create a merge commit.`,
|
|
9057
9294
|
approvalRequired,
|
|
9058
9295
|
command
|
|
9059
9296
|
),
|
|
@@ -9991,35 +10228,21 @@ var DEFAULT_MANAGED_DOC_FILES = [
|
|
|
9991
10228
|
".gitignore"
|
|
9992
10229
|
];
|
|
9993
10230
|
|
|
9994
|
-
// src/utils/commit-conventions.ts
|
|
9995
|
-
var PROJECT_COMMIT_PREFIX_PATTERN = /^(feat|fix|refactor|test|chore)\(#(\d+)\):\s+\S.+$/i;
|
|
9996
|
-
var DOCS_COMMIT_PREFIX_PATTERN = /^docs\(#(\d+)\):\s+\S.+$/i;
|
|
9997
|
-
function matchesProjectCommitConvention(message, issueNumber) {
|
|
9998
|
-
const normalized = String(message || "").trim();
|
|
9999
|
-
if (!normalized) return false;
|
|
10000
|
-
const match = normalized.match(PROJECT_COMMIT_PREFIX_PATTERN);
|
|
10001
|
-
if (!match) return false;
|
|
10002
|
-
return Number(match[2]) === issueNumber;
|
|
10003
|
-
}
|
|
10004
|
-
function matchesDocsCommitConvention(message, issueNumber) {
|
|
10005
|
-
const normalized = String(message || "").trim();
|
|
10006
|
-
if (!normalized) return false;
|
|
10007
|
-
const match = normalized.match(DOCS_COMMIT_PREFIX_PATTERN);
|
|
10008
|
-
if (!match) return false;
|
|
10009
|
-
return Number(match[1]) === issueNumber;
|
|
10010
|
-
}
|
|
10011
|
-
|
|
10012
10231
|
// src/commands/commit-audit.ts
|
|
10013
10232
|
var CANONICAL_FEATURE_DOC_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(spec|plan|tasks|decisions|issue|pr)\.md$/i;
|
|
10014
10233
|
var FEATURE_DOC_CANDIDATE_PATTERN = /^features\/(?:[^/]+\/)?F\d{3,}[^/]*\/(.+)$/i;
|
|
10015
10234
|
function commitAuditCommand(program2) {
|
|
10016
|
-
program2.command("commit-audit").description("Validate staged docs paths before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").action(async (options) => {
|
|
10235
|
+
program2.command("commit-audit").description("Validate staged docs paths and canonical commit subjects before commit").option("--json", "Output JSON for hooks and agents").option("--git-root <path>", "Override the git root used for staged-path inspection").option("--message <message>", "Validate a commit subject against the current workflow convention").option("--message-file <path>", "Read and validate the commit subject from a commit message file").option("--enforce", "Exit non-zero when commit-audit blocks the commit").action(async (options) => {
|
|
10017
10236
|
try {
|
|
10237
|
+
const commitMessage = await resolveCommitMessageInput(process.cwd(), options);
|
|
10018
10238
|
const payload = await collectCommitAudit(
|
|
10019
10239
|
process.cwd(),
|
|
10020
10240
|
options.gitRoot,
|
|
10021
|
-
|
|
10241
|
+
commitMessage
|
|
10022
10242
|
);
|
|
10243
|
+
if (options.enforce && payload.status === "blocked") {
|
|
10244
|
+
process.exitCode = 1;
|
|
10245
|
+
}
|
|
10023
10246
|
if (options.json) {
|
|
10024
10247
|
console.log(JSON.stringify(payload, null, 2));
|
|
10025
10248
|
return;
|
|
@@ -10046,6 +10269,7 @@ function commitAuditCommand(program2) {
|
|
|
10046
10269
|
2
|
|
10047
10270
|
)
|
|
10048
10271
|
);
|
|
10272
|
+
if (options.enforce) process.exitCode = 1;
|
|
10049
10273
|
return;
|
|
10050
10274
|
}
|
|
10051
10275
|
process.stderr.write(`[${cliError.code}] ${cliError.message}
|
|
@@ -10054,6 +10278,18 @@ function commitAuditCommand(program2) {
|
|
|
10054
10278
|
}
|
|
10055
10279
|
});
|
|
10056
10280
|
}
|
|
10281
|
+
async function resolveCommitMessageInput(cwd, options) {
|
|
10282
|
+
if (options.message && options.messageFile) {
|
|
10283
|
+
throw createCliError(
|
|
10284
|
+
"INVALID_ARGUMENT",
|
|
10285
|
+
"Use either --message or --message-file, not both."
|
|
10286
|
+
);
|
|
10287
|
+
}
|
|
10288
|
+
if (options.message) return options.message;
|
|
10289
|
+
if (!options.messageFile) return void 0;
|
|
10290
|
+
const content = await fs26.readFile(path8.resolve(cwd, options.messageFile), "utf-8");
|
|
10291
|
+
return content.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
10292
|
+
}
|
|
10057
10293
|
async function collectCommitAudit(cwd, gitRootOverride, commitMessage) {
|
|
10058
10294
|
const config = await getConfig(cwd);
|
|
10059
10295
|
if (!config) {
|
|
@@ -10277,10 +10513,17 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
10277
10513
|
return null;
|
|
10278
10514
|
}
|
|
10279
10515
|
const selection = await resolveFeatureSelection(cwd);
|
|
10280
|
-
if (selection.status !== "selected" || !selection.matchedFeature
|
|
10516
|
+
if (selection.status !== "selected" || !selection.matchedFeature) {
|
|
10517
|
+
return null;
|
|
10518
|
+
}
|
|
10519
|
+
const scope = resolveFeatureCommitScope({
|
|
10520
|
+
issueNumber: selection.matchedFeature.issueNumber,
|
|
10521
|
+
featureId: selection.matchedFeature.id,
|
|
10522
|
+
workflowMode: config.workflow?.mode
|
|
10523
|
+
});
|
|
10524
|
+
if (!scope) {
|
|
10281
10525
|
return null;
|
|
10282
10526
|
}
|
|
10283
|
-
const issueNumber = selection.matchedFeature.issueNumber;
|
|
10284
10527
|
const docsRepoRoot = runGitCapture(["rev-parse", "--show-toplevel"], config.docsDir);
|
|
10285
10528
|
const normalizedRepoRoot = path8.resolve(repoRoot);
|
|
10286
10529
|
const normalizedDocsRepoRoot = docsRepoRoot ? path8.resolve(docsRepoRoot) : null;
|
|
@@ -10290,15 +10533,15 @@ async function collectCommitMessageViolation(cwd, config, repoRoot, stagedEntrie
|
|
|
10290
10533
|
return !!relativeToDocs && relativeToDocs !== "" && !relativeToDocs.startsWith("..");
|
|
10291
10534
|
});
|
|
10292
10535
|
const isDocsCommit = !!normalizedDocsRepoRoot && normalizedDocsRepoRoot === normalizedRepoRoot && (config.docsRepo === "standalone" || docsOnlyCommit);
|
|
10293
|
-
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage,
|
|
10536
|
+
const valid = isDocsCommit ? matchesDocsCommitConvention(normalizedMessage, scope) : matchesProjectCommitConvention(normalizedMessage, scope);
|
|
10294
10537
|
if (valid) {
|
|
10295
10538
|
return null;
|
|
10296
10539
|
}
|
|
10297
|
-
const expected = isDocsCommit ? `docs(
|
|
10540
|
+
const expected = isDocsCommit ? `docs(${scope}): ...` : `type(${scope}): ... (feat/fix/refactor/test/style/chore)`;
|
|
10298
10541
|
return {
|
|
10299
10542
|
path: "(commit message)",
|
|
10300
10543
|
kind: "commit_message_policy",
|
|
10301
|
-
detail: `Commit subject must follow the
|
|
10544
|
+
detail: `Commit subject must follow the canonical feature-scoped convention. Expected ${expected}, received "${normalizedMessage}".`
|
|
10302
10545
|
};
|
|
10303
10546
|
}
|
|
10304
10547
|
function normalizeEntryName(value) {
|
|
@@ -10609,7 +10852,15 @@ async function collectDocsAudit(cwd) {
|
|
|
10609
10852
|
// src/commands/local.ts
|
|
10610
10853
|
function localCommand(program2) {
|
|
10611
10854
|
const local = program2.command("local").description("Integrate and clean up local workflow feature branches");
|
|
10612
|
-
local.command("
|
|
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
|
+
);
|
|
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(
|
|
10613
10864
|
"--confirm <token>",
|
|
10614
10865
|
"Approval token when local_merge requires approval"
|
|
10615
10866
|
).option("--json", "Output JSON for agents and hooks").action(
|
|
@@ -10629,6 +10880,111 @@ function localCommand(program2) {
|
|
|
10629
10880
|
}
|
|
10630
10881
|
);
|
|
10631
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
|
+
}
|
|
10632
10988
|
async function runLocalAction(options, action) {
|
|
10633
10989
|
try {
|
|
10634
10990
|
const payload = await action();
|
|
@@ -10700,6 +11056,15 @@ async function runLocalMerge(featureName, options) {
|
|
|
10700
11056
|
"Both the configured base branch and Feature tip must exist."
|
|
10701
11057
|
);
|
|
10702
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
|
+
}
|
|
10703
11068
|
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
10704
11069
|
return blocked(
|
|
10705
11070
|
"LOCAL_MERGE_DIRTY_WORKTREE",
|
|
@@ -10708,7 +11073,9 @@ async function runLocalMerge(featureName, options) {
|
|
|
10708
11073
|
);
|
|
10709
11074
|
}
|
|
10710
11075
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10711
|
-
|
|
11076
|
+
const originalBaseTip = context.baseTip;
|
|
11077
|
+
const previousStateStrategy = context.state?.strategy || "local-ff";
|
|
11078
|
+
let state = context.state && context.state.featureTip === context.featureTip && previousStateStrategy === context.completionStrategy ? context.state : {
|
|
10712
11079
|
version: 1,
|
|
10713
11080
|
featureRef: feature.folderName,
|
|
10714
11081
|
component: feature.type,
|
|
@@ -10716,20 +11083,39 @@ async function runLocalMerge(featureName, options) {
|
|
|
10716
11083
|
featureBranch: context.featureBranch,
|
|
10717
11084
|
featureTip: context.featureTip,
|
|
10718
11085
|
mergedBaseTip: context.baseTip,
|
|
11086
|
+
strategy: context.completionStrategy,
|
|
10719
11087
|
status: "merged",
|
|
10720
11088
|
mergedAt: now,
|
|
10721
11089
|
verifiedAt: null,
|
|
10722
11090
|
cleanedAt: null,
|
|
10723
11091
|
verification: []
|
|
10724
11092
|
};
|
|
10725
|
-
if (!context.
|
|
11093
|
+
if (context.completionStrategy === "local-squash" && context.squashCommitMatchesSource && !context.squashEvidencePresent) {
|
|
11094
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
11095
|
+
context.projectRoot,
|
|
11096
|
+
context.evidenceRef,
|
|
11097
|
+
context.featureTip
|
|
11098
|
+
);
|
|
11099
|
+
if (evidence.code !== 0) {
|
|
11100
|
+
return blocked(
|
|
11101
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
11102
|
+
feature.folderName,
|
|
11103
|
+
evidence.stderr
|
|
11104
|
+
);
|
|
11105
|
+
}
|
|
11106
|
+
context = await resolveLocalIntegrationContext(config, feature);
|
|
11107
|
+
}
|
|
11108
|
+
if (!context.featureTip || !context.baseTip) {
|
|
11109
|
+
return blocked("LOCAL_MERGE_REF_NOT_FOUND", feature.folderName);
|
|
11110
|
+
}
|
|
11111
|
+
if (!context.integrationComplete) {
|
|
10726
11112
|
if (!isAncestor(
|
|
10727
11113
|
context.projectRoot,
|
|
10728
11114
|
`refs/heads/${context.baseBranch}`,
|
|
10729
11115
|
context.featureTip
|
|
10730
11116
|
)) {
|
|
10731
11117
|
return blocked(
|
|
10732
|
-
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
11118
|
+
context.completionStrategy === "local-squash" ? "LOCAL_SQUASH_BASE_NOT_ANCESTOR" : "LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10733
11119
|
feature.folderName,
|
|
10734
11120
|
`${context.baseBranch} is not an ancestor of ${context.featureBranch}.`
|
|
10735
11121
|
);
|
|
@@ -10747,26 +11133,22 @@ async function runLocalMerge(featureName, options) {
|
|
|
10747
11133
|
);
|
|
10748
11134
|
}
|
|
10749
11135
|
}
|
|
10750
|
-
const
|
|
10751
|
-
|
|
10752
|
-
"--ff-only",
|
|
10753
|
-
context.featureBranch
|
|
10754
|
-
]);
|
|
10755
|
-
if (merge.code !== 0) {
|
|
10756
|
-
return blocked(
|
|
10757
|
-
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
10758
|
-
feature.folderName,
|
|
10759
|
-
merge.stderr
|
|
10760
|
-
);
|
|
10761
|
-
}
|
|
11136
|
+
const integration = context.completionStrategy === "local-squash" ? integrateLocalSquash(context, feature.folderName, feature.id, feature.slug) : integrateLocalFastForward(context, feature.folderName);
|
|
11137
|
+
if (integration.status !== "ok") return integration.payload;
|
|
10762
11138
|
const mergedBaseTip = resolveRef(
|
|
10763
11139
|
context.projectRoot,
|
|
10764
11140
|
`refs/heads/${context.baseBranch}`
|
|
10765
11141
|
);
|
|
11142
|
+
const integratedTree = mergedBaseTip ? resolveTree(context.projectRoot, mergedBaseTip) : null;
|
|
10766
11143
|
state = {
|
|
10767
11144
|
...state,
|
|
10768
11145
|
featureTip: context.featureTip,
|
|
10769
11146
|
mergedBaseTip: mergedBaseTip || context.featureTip,
|
|
11147
|
+
strategy: context.completionStrategy,
|
|
11148
|
+
integratedCommit: mergedBaseTip || context.featureTip,
|
|
11149
|
+
originalBaseTip,
|
|
11150
|
+
...integratedTree ? { integratedTree } : {},
|
|
11151
|
+
...context.completionStrategy === "local-squash" ? { evidenceRef: context.evidenceRef } : {},
|
|
10770
11152
|
status: "merged",
|
|
10771
11153
|
mergedAt: now,
|
|
10772
11154
|
verifiedAt: null,
|
|
@@ -10776,19 +11158,39 @@ async function runLocalMerge(featureName, options) {
|
|
|
10776
11158
|
await writeLocalIntegrationState(context.projectRoot, feature, state);
|
|
10777
11159
|
context = await resolveLocalIntegrationContext(config, feature);
|
|
10778
11160
|
}
|
|
10779
|
-
if (!context.
|
|
11161
|
+
if (!context.integrationComplete) {
|
|
10780
11162
|
return blocked("LOCAL_MERGE_VERIFICATION_FAILED", feature.folderName);
|
|
10781
11163
|
}
|
|
10782
|
-
const
|
|
11164
|
+
const integratedTip = resolveRef(
|
|
10783
11165
|
context.projectRoot,
|
|
10784
|
-
context.
|
|
11166
|
+
`refs/heads/${context.baseBranch}`
|
|
10785
11167
|
);
|
|
10786
|
-
const
|
|
11168
|
+
const verificationTarget = integratedTip || state.featureTip;
|
|
11169
|
+
const postMergeVerification = runLocalChecks(
|
|
11170
|
+
context.projectRoot,
|
|
11171
|
+
context.postMergeChecks,
|
|
11172
|
+
verificationTarget,
|
|
11173
|
+
resolveLocalIntegrationLogDir(
|
|
11174
|
+
context.projectRoot,
|
|
11175
|
+
feature,
|
|
11176
|
+
verificationTarget
|
|
11177
|
+
),
|
|
11178
|
+
"post-merge"
|
|
11179
|
+
);
|
|
11180
|
+
const featureVerification = state.featureVerification || context.state?.featureVerification || [];
|
|
11181
|
+
const verification = [...featureVerification, ...postMergeVerification];
|
|
11182
|
+
const failedCheck = postMergeVerification.find((entry) => entry.exitCode !== 0);
|
|
10787
11183
|
if (failedCheck) {
|
|
11184
|
+
rollbackLocalIntegration(context, originalBaseTip);
|
|
10788
11185
|
await writeLocalIntegrationState(context.projectRoot, feature, {
|
|
10789
11186
|
...state,
|
|
10790
|
-
status: "
|
|
11187
|
+
status: "feature_failed",
|
|
11188
|
+
mergedBaseTip: originalBaseTip,
|
|
11189
|
+
integratedCommit: void 0,
|
|
11190
|
+
integratedTree: void 0,
|
|
10791
11191
|
verification,
|
|
11192
|
+
featureVerification,
|
|
11193
|
+
postMergeVerification,
|
|
10792
11194
|
verifiedAt: null
|
|
10793
11195
|
});
|
|
10794
11196
|
return {
|
|
@@ -10814,7 +11216,9 @@ async function runLocalMerge(featureName, options) {
|
|
|
10814
11216
|
mergedBaseTip: baseTip || verifiedFeatureTip,
|
|
10815
11217
|
status: "verified",
|
|
10816
11218
|
verifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10817
|
-
verification
|
|
11219
|
+
verification,
|
|
11220
|
+
featureVerification,
|
|
11221
|
+
postMergeVerification
|
|
10818
11222
|
};
|
|
10819
11223
|
await writeLocalIntegrationState(context.projectRoot, feature, verifiedState);
|
|
10820
11224
|
return {
|
|
@@ -10825,9 +11229,19 @@ async function runLocalMerge(featureName, options) {
|
|
|
10825
11229
|
featureBranch: context.featureBranch,
|
|
10826
11230
|
featureTip: context.featureTip,
|
|
10827
11231
|
baseTip,
|
|
11232
|
+
completionStrategy: context.completionStrategy,
|
|
10828
11233
|
verification
|
|
10829
11234
|
};
|
|
10830
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
|
+
}
|
|
10831
11245
|
async function runLocalCleanup(featureName, options) {
|
|
10832
11246
|
const cwd = process.cwd();
|
|
10833
11247
|
const stage = await collectWorkflowStage(cwd, featureName, options.component);
|
|
@@ -10855,7 +11269,7 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10855
11269
|
}
|
|
10856
11270
|
const feature = selection.matchedFeature;
|
|
10857
11271
|
let context = await resolveLocalIntegrationContext(config, feature);
|
|
10858
|
-
if (!context.
|
|
11272
|
+
if (!context.integrationComplete || !context.state || !["verified", "cleaned"].includes(context.state.status) || context.state.featureTip !== context.featureTip || context.state.mergedBaseTip !== context.baseTip) {
|
|
10859
11273
|
return blocked("LOCAL_MERGE_NOT_VERIFIED", feature.folderName);
|
|
10860
11274
|
}
|
|
10861
11275
|
if (!context.featureWorktreeClean || !context.projectRootClean || !context.docsClean) {
|
|
@@ -10881,7 +11295,7 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10881
11295
|
if (context.deleteFeatureBranchAfterMerge && context.featureBranchExists) {
|
|
10882
11296
|
const removeBranch = gitRun(context.projectRoot, [
|
|
10883
11297
|
"branch",
|
|
10884
|
-
"-d",
|
|
11298
|
+
context.completionStrategy === "local-squash" ? "-D" : "-d",
|
|
10885
11299
|
context.featureBranch
|
|
10886
11300
|
]);
|
|
10887
11301
|
if (removeBranch.code !== 0) {
|
|
@@ -10905,9 +11319,106 @@ async function runLocalCleanup(featureName, options) {
|
|
|
10905
11319
|
baseBranch: context.baseBranch,
|
|
10906
11320
|
featureBranch: context.featureBranch,
|
|
10907
11321
|
featureTip: context.featureTip,
|
|
10908
|
-
baseTip: context.baseTip
|
|
11322
|
+
baseTip: context.baseTip,
|
|
11323
|
+
completionStrategy: context.completionStrategy
|
|
10909
11324
|
};
|
|
10910
11325
|
}
|
|
11326
|
+
function integrateLocalFastForward(context, featureRef) {
|
|
11327
|
+
const merge = gitRun(context.projectRoot, [
|
|
11328
|
+
"merge",
|
|
11329
|
+
"--ff-only",
|
|
11330
|
+
context.featureBranch
|
|
11331
|
+
]);
|
|
11332
|
+
if (merge.code !== 0) {
|
|
11333
|
+
return {
|
|
11334
|
+
status: "blocked",
|
|
11335
|
+
payload: blocked(
|
|
11336
|
+
"LOCAL_MERGE_NOT_FAST_FORWARD",
|
|
11337
|
+
featureRef,
|
|
11338
|
+
merge.stderr
|
|
11339
|
+
)
|
|
11340
|
+
};
|
|
11341
|
+
}
|
|
11342
|
+
return { status: "ok" };
|
|
11343
|
+
}
|
|
11344
|
+
function integrateLocalSquash(context, featureRef, featureId, featureSlug) {
|
|
11345
|
+
const originalBaseTip = context.baseTip;
|
|
11346
|
+
if (!originalBaseTip || !context.featureTip) {
|
|
11347
|
+
return {
|
|
11348
|
+
status: "blocked",
|
|
11349
|
+
payload: blocked("LOCAL_MERGE_REF_NOT_FOUND", featureRef)
|
|
11350
|
+
};
|
|
11351
|
+
}
|
|
11352
|
+
const squash = gitRun(context.projectRoot, [
|
|
11353
|
+
"merge",
|
|
11354
|
+
"--squash",
|
|
11355
|
+
context.featureBranch
|
|
11356
|
+
]);
|
|
11357
|
+
if (squash.code !== 0) {
|
|
11358
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11359
|
+
return {
|
|
11360
|
+
status: "blocked",
|
|
11361
|
+
payload: blocked("LOCAL_SQUASH_FAILED", featureRef, squash.stderr)
|
|
11362
|
+
};
|
|
11363
|
+
}
|
|
11364
|
+
const scope = resolveFeatureCommitScope({
|
|
11365
|
+
featureId,
|
|
11366
|
+
workflowMode: "local"
|
|
11367
|
+
});
|
|
11368
|
+
if (!scope) {
|
|
11369
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11370
|
+
return {
|
|
11371
|
+
status: "blocked",
|
|
11372
|
+
payload: blocked(
|
|
11373
|
+
"LOCAL_SQUASH_COMMIT_SCOPE_INVALID",
|
|
11374
|
+
featureRef,
|
|
11375
|
+
`Could not resolve a canonical local commit scope from ${featureId}.`
|
|
11376
|
+
)
|
|
11377
|
+
};
|
|
11378
|
+
}
|
|
11379
|
+
const commit = gitRun(context.projectRoot, [
|
|
11380
|
+
"commit",
|
|
11381
|
+
"-m",
|
|
11382
|
+
`feat(${scope}): integrate ${featureSlug}`
|
|
11383
|
+
]);
|
|
11384
|
+
if (commit.code !== 0) {
|
|
11385
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11386
|
+
return {
|
|
11387
|
+
status: "blocked",
|
|
11388
|
+
payload: blocked("LOCAL_SQUASH_COMMIT_FAILED", featureRef, commit.stderr)
|
|
11389
|
+
};
|
|
11390
|
+
}
|
|
11391
|
+
const integratedCommit = resolveRef(context.projectRoot, "HEAD");
|
|
11392
|
+
const sourceTree = resolveTree(context.projectRoot, context.featureTip);
|
|
11393
|
+
const integratedTree = integratedCommit ? resolveTree(context.projectRoot, integratedCommit) : null;
|
|
11394
|
+
if (!integratedCommit || !sourceTree || integratedTree !== sourceTree) {
|
|
11395
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11396
|
+
return {
|
|
11397
|
+
status: "blocked",
|
|
11398
|
+
payload: blocked("LOCAL_SQUASH_TREE_MISMATCH", featureRef)
|
|
11399
|
+
};
|
|
11400
|
+
}
|
|
11401
|
+
const evidence = preserveLocalIntegrationEvidence(
|
|
11402
|
+
context.projectRoot,
|
|
11403
|
+
context.evidenceRef,
|
|
11404
|
+
context.featureTip
|
|
11405
|
+
);
|
|
11406
|
+
if (evidence.code !== 0) {
|
|
11407
|
+
rollbackSquash(context.projectRoot, originalBaseTip);
|
|
11408
|
+
return {
|
|
11409
|
+
status: "blocked",
|
|
11410
|
+
payload: blocked(
|
|
11411
|
+
"LOCAL_SQUASH_EVIDENCE_FAILED",
|
|
11412
|
+
featureRef,
|
|
11413
|
+
evidence.stderr
|
|
11414
|
+
)
|
|
11415
|
+
};
|
|
11416
|
+
}
|
|
11417
|
+
return { status: "ok" };
|
|
11418
|
+
}
|
|
11419
|
+
function rollbackSquash(projectRoot, originalBaseTip) {
|
|
11420
|
+
gitRun(projectRoot, ["reset", "--hard", originalBaseTip]);
|
|
11421
|
+
}
|
|
10911
11422
|
function blocked(reasonCode, featureRef, error) {
|
|
10912
11423
|
return {
|
|
10913
11424
|
status: "blocked",
|