@lumenflow/core 1.0.0
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/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU Status Transition Module
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for block/unblock status transitions.
|
|
5
|
+
* Eliminates 90% code duplication between wu-block.mjs and wu-unblock.mjs.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Validate state transitions (via state-machine)
|
|
9
|
+
* - Update WU YAML status and notes
|
|
10
|
+
* - Sync backlog.md and status.md
|
|
11
|
+
* - Handle worktree creation/removal
|
|
12
|
+
*
|
|
13
|
+
* Created: WU-1340 (2025-11-29)
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Direction type for status transitions
|
|
17
|
+
* @typedef {'block' | 'unblock'} TransitionDirection
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Transition WU status between blocked and in_progress states
|
|
21
|
+
*
|
|
22
|
+
* @param {object} options - Transition options
|
|
23
|
+
* @param {string} options.id - WU ID (e.g., 'WU-100')
|
|
24
|
+
* @param {TransitionDirection} options.direction - Transition direction: 'block' or 'unblock'
|
|
25
|
+
* @param {string} [options.reason] - Reason for transition (optional)
|
|
26
|
+
* @param {string} [options.worktreeOverride] - Custom worktree path (optional)
|
|
27
|
+
* @param {boolean} [options.removeWorktree] - Remove worktree after blocking (default: false)
|
|
28
|
+
* @param {boolean} [options.createWorktree] - Create worktree after unblocking (default: false)
|
|
29
|
+
* @param {object} [options.gitAdapter] - Git adapter for testing (optional, defaults to getGitForCwd())
|
|
30
|
+
* @returns {{id: string, fromStatus: string, toStatus: string}} Transition result
|
|
31
|
+
* @throws {Error} If state transition is invalid or files not found
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Block a WU
|
|
35
|
+
* transitionWUStatus({
|
|
36
|
+
* id: 'WU-100',
|
|
37
|
+
* direction: 'block',
|
|
38
|
+
* reason: 'Blocked by WU-200',
|
|
39
|
+
* removeWorktree: true
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Unblock a WU
|
|
44
|
+
* transitionWUStatus({
|
|
45
|
+
* id: 'WU-100',
|
|
46
|
+
* direction: 'unblock',
|
|
47
|
+
* reason: 'Blocker resolved',
|
|
48
|
+
* createWorktree: true
|
|
49
|
+
* });
|
|
50
|
+
*/
|
|
51
|
+
export declare function transitionWUStatus({ id, direction, reason, worktreeOverride, removeWorktree, createWorktree, gitAdapter, }: {
|
|
52
|
+
id: any;
|
|
53
|
+
direction: any;
|
|
54
|
+
reason: any;
|
|
55
|
+
worktreeOverride: any;
|
|
56
|
+
removeWorktree?: boolean;
|
|
57
|
+
createWorktree?: boolean;
|
|
58
|
+
gitAdapter: any;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
id: any;
|
|
61
|
+
fromStatus: any;
|
|
62
|
+
toStatus: string;
|
|
63
|
+
}>;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU Status Transition Module
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for block/unblock status transitions.
|
|
5
|
+
* Eliminates 90% code duplication between wu-block.mjs and wu-unblock.mjs.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Validate state transitions (via state-machine)
|
|
9
|
+
* - Update WU YAML status and notes
|
|
10
|
+
* - Sync backlog.md and status.md
|
|
11
|
+
* - Handle worktree creation/removal
|
|
12
|
+
*
|
|
13
|
+
* Created: WU-1340 (2025-11-29)
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { assertTransition } from './state-machine.js';
|
|
17
|
+
// WU-1574: Removed BacklogManager - using state store + generator
|
|
18
|
+
import { WUStateStore } from './wu-state-store.js';
|
|
19
|
+
import { generateBacklog } from './backlog-generator.js';
|
|
20
|
+
import { writeFile } from 'node:fs/promises';
|
|
21
|
+
import { parseBacklogFrontmatter, getSectionHeadings } from './backlog-parser.js';
|
|
22
|
+
import { createError, ErrorCodes } from './error-handler.js';
|
|
23
|
+
import { todayISO } from './date-utils.js';
|
|
24
|
+
import { getStateStoreDirFromBacklog, WU_PATHS } from './wu-paths.js';
|
|
25
|
+
import { readWU, writeWU, appendNote } from './wu-yaml.js';
|
|
26
|
+
import { toKebab, REMOTES, BRANCHES, WU_STATUS, STRING_LITERALS, } from './wu-constants.js';
|
|
27
|
+
/**
|
|
28
|
+
* Direction type for status transitions
|
|
29
|
+
* @typedef {'block' | 'unblock'} TransitionDirection
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Transition WU status between blocked and in_progress states
|
|
33
|
+
*
|
|
34
|
+
* @param {object} options - Transition options
|
|
35
|
+
* @param {string} options.id - WU ID (e.g., 'WU-100')
|
|
36
|
+
* @param {TransitionDirection} options.direction - Transition direction: 'block' or 'unblock'
|
|
37
|
+
* @param {string} [options.reason] - Reason for transition (optional)
|
|
38
|
+
* @param {string} [options.worktreeOverride] - Custom worktree path (optional)
|
|
39
|
+
* @param {boolean} [options.removeWorktree] - Remove worktree after blocking (default: false)
|
|
40
|
+
* @param {boolean} [options.createWorktree] - Create worktree after unblocking (default: false)
|
|
41
|
+
* @param {object} [options.gitAdapter] - Git adapter for testing (optional, defaults to getGitForCwd())
|
|
42
|
+
* @returns {{id: string, fromStatus: string, toStatus: string}} Transition result
|
|
43
|
+
* @throws {Error} If state transition is invalid or files not found
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Block a WU
|
|
47
|
+
* transitionWUStatus({
|
|
48
|
+
* id: 'WU-100',
|
|
49
|
+
* direction: 'block',
|
|
50
|
+
* reason: 'Blocked by WU-200',
|
|
51
|
+
* removeWorktree: true
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Unblock a WU
|
|
56
|
+
* transitionWUStatus({
|
|
57
|
+
* id: 'WU-100',
|
|
58
|
+
* direction: 'unblock',
|
|
59
|
+
* reason: 'Blocker resolved',
|
|
60
|
+
* createWorktree: true
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
// WU-1574: Made async for updateBacklogAndStatus
|
|
64
|
+
export async function transitionWUStatus({ id, direction, reason, worktreeOverride, removeWorktree = false, createWorktree = false, gitAdapter, }) {
|
|
65
|
+
// Validate inputs
|
|
66
|
+
if (!id) {
|
|
67
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, 'WU ID is required');
|
|
68
|
+
}
|
|
69
|
+
if (!direction || !['block', 'unblock'].includes(direction)) {
|
|
70
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `Invalid direction: ${direction}. Must be 'block' or 'unblock'`);
|
|
71
|
+
}
|
|
72
|
+
// Resolve paths
|
|
73
|
+
const paths = {
|
|
74
|
+
wu: WU_PATHS.WU(id),
|
|
75
|
+
status: WU_PATHS.STATUS(),
|
|
76
|
+
backlog: WU_PATHS.BACKLOG(),
|
|
77
|
+
};
|
|
78
|
+
// Validate files exist
|
|
79
|
+
if (!existsSync(paths.wu)) {
|
|
80
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `WU file not found: ${paths.wu}`, {
|
|
81
|
+
path: paths.wu,
|
|
82
|
+
id,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!existsSync(paths.backlog)) {
|
|
86
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Missing ${paths.backlog}`, {
|
|
87
|
+
path: paths.backlog,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!existsSync(paths.status)) {
|
|
91
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Missing ${paths.status}`, {
|
|
92
|
+
path: paths.status,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Read WU document
|
|
96
|
+
const doc = readWU(paths.wu, id);
|
|
97
|
+
const title = doc.title || '';
|
|
98
|
+
const currentStatus = doc.status || WU_STATUS.IN_PROGRESS;
|
|
99
|
+
// Determine target status
|
|
100
|
+
const toStatus = direction === 'block' ? WU_STATUS.BLOCKED : WU_STATUS.IN_PROGRESS;
|
|
101
|
+
// Validate state transition (may throw if invalid)
|
|
102
|
+
try {
|
|
103
|
+
assertTransition(currentStatus, toStatus, id);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// If already in target state, make operation idempotent (don't throw)
|
|
107
|
+
if (currentStatus === toStatus) {
|
|
108
|
+
console.warn(`[wu-status-transition] WU ${id} already ${toStatus}, skipping transition (idempotent)`);
|
|
109
|
+
return { id, fromStatus: currentStatus, toStatus };
|
|
110
|
+
}
|
|
111
|
+
// Re-throw validation errors
|
|
112
|
+
throw createError(ErrorCodes.STATE_ERROR, `State transition validation failed: ${error.message}`, { id, fromStatus: currentStatus, toStatus, originalError: error.message });
|
|
113
|
+
}
|
|
114
|
+
// Update WU YAML
|
|
115
|
+
doc.status = toStatus;
|
|
116
|
+
const noteLine = createNoteEntry(direction, reason);
|
|
117
|
+
appendNote(doc, noteLine);
|
|
118
|
+
writeWU(paths.wu, doc);
|
|
119
|
+
// Update backlog.md and status.md (WU-1574: now async)
|
|
120
|
+
await updateBacklogAndStatus(paths, id, title, currentStatus, toStatus, direction, reason);
|
|
121
|
+
// Handle worktree operations (only if gitAdapter provided)
|
|
122
|
+
if (gitAdapter) {
|
|
123
|
+
if (direction === 'block' && removeWorktree) {
|
|
124
|
+
handleWorktreeRemoval(doc, worktreeOverride, gitAdapter);
|
|
125
|
+
}
|
|
126
|
+
else if (direction === 'unblock' && createWorktree) {
|
|
127
|
+
handleWorktreeCreation(doc, worktreeOverride, gitAdapter);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
fromStatus: currentStatus,
|
|
133
|
+
toStatus,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Create note entry for transition
|
|
138
|
+
*
|
|
139
|
+
* @private
|
|
140
|
+
* @param {TransitionDirection} direction - Transition direction
|
|
141
|
+
* @param {string} [reason] - Transition reason
|
|
142
|
+
* @returns {string} Note text
|
|
143
|
+
*/
|
|
144
|
+
function createNoteEntry(direction, reason) {
|
|
145
|
+
const action = direction === 'block' ? 'Blocked' : 'Unblocked';
|
|
146
|
+
const date = todayISO();
|
|
147
|
+
return reason ? `${action} (${date}): ${reason}` : `${action} (${date})`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Update backlog.md and status.md files
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @param {object} paths - File paths
|
|
154
|
+
* @param {string} id - WU ID
|
|
155
|
+
* @param {string} title - WU title
|
|
156
|
+
* @param {string} fromStatus - Current status
|
|
157
|
+
* @param {string} toStatus - Target status
|
|
158
|
+
* @param {TransitionDirection} direction - Transition direction
|
|
159
|
+
* @param {string} [reason] - Transition reason
|
|
160
|
+
*/
|
|
161
|
+
// WU-1574: Made async for generateBacklog
|
|
162
|
+
async function updateBacklogAndStatus(paths, id, title, fromStatus, toStatus, direction, reason) {
|
|
163
|
+
// Parse frontmatter to get section headings
|
|
164
|
+
let frontmatter;
|
|
165
|
+
try {
|
|
166
|
+
({ frontmatter } = parseBacklogFrontmatter(paths.backlog));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
throw createError(ErrorCodes.YAML_PARSE_ERROR, `Failed to parse backlog frontmatter: ${err.message}`, { path: paths.backlog, originalError: err.message });
|
|
170
|
+
}
|
|
171
|
+
const headings = frontmatter ? getSectionHeadings(frontmatter) : {};
|
|
172
|
+
const inProgressHeading = headings.in_progress || '## 🔧 In progress';
|
|
173
|
+
const blockedHeading = headings.blocked || '## ⛔ Blocked';
|
|
174
|
+
// Determine source and target sections
|
|
175
|
+
let fromSection, toSection, format;
|
|
176
|
+
if (direction === 'block') {
|
|
177
|
+
fromSection = inProgressHeading;
|
|
178
|
+
toSection = blockedHeading;
|
|
179
|
+
format = 'blocked';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
fromSection = blockedHeading;
|
|
183
|
+
toSection = inProgressHeading;
|
|
184
|
+
format = 'progress';
|
|
185
|
+
}
|
|
186
|
+
// WU-1574: Regenerate backlog.md from state store (replaces BacklogManager)
|
|
187
|
+
const stateDir = getStateStoreDirFromBacklog(paths.backlog);
|
|
188
|
+
const store = new WUStateStore(stateDir);
|
|
189
|
+
await store.load();
|
|
190
|
+
const content = await generateBacklog(store);
|
|
191
|
+
await writeFile(paths.backlog, content, 'utf-8');
|
|
192
|
+
// Update status.md
|
|
193
|
+
updateStatusFile(paths.status, id, title, direction, reason);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Update status.md file
|
|
197
|
+
*
|
|
198
|
+
* @private
|
|
199
|
+
* @param {string} statusPath - Path to status.md
|
|
200
|
+
* @param {string} id - WU ID
|
|
201
|
+
* @param {string} title - WU title
|
|
202
|
+
* @param {TransitionDirection} direction - Transition direction
|
|
203
|
+
* @param {string} [reason] - Transition reason
|
|
204
|
+
*/
|
|
205
|
+
function updateStatusFile(statusPath, id, title, direction, reason) {
|
|
206
|
+
const rel = `wu/${id}.yaml`;
|
|
207
|
+
const lines = readFileSync(statusPath, { encoding: 'utf-8' }).split(/\r?\n/);
|
|
208
|
+
const findHeader = (h) => lines.findIndex((l) => l.trim().toLowerCase() === h.toLowerCase());
|
|
209
|
+
const inProgIdx = findHeader('## in progress');
|
|
210
|
+
const blockedIdx = findHeader('## blocked');
|
|
211
|
+
if (direction === 'block') {
|
|
212
|
+
// Remove from In Progress
|
|
213
|
+
removeFromSection(lines, inProgIdx, rel, id);
|
|
214
|
+
// Add to Blocked
|
|
215
|
+
if (blockedIdx !== -1) {
|
|
216
|
+
const reasonSuffix = reason ? ` — ${reason}` : '';
|
|
217
|
+
const bullet = `- [${id} — ${title}](${rel})${reasonSuffix}`;
|
|
218
|
+
const sectionStart = blockedIdx + 1;
|
|
219
|
+
// Check if already exists (idempotent)
|
|
220
|
+
if (!lines.slice(sectionStart).some((l) => l.includes(rel))) {
|
|
221
|
+
lines.splice(sectionStart, 0, '', bullet);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Remove from Blocked
|
|
227
|
+
removeFromSection(lines, blockedIdx, rel, id);
|
|
228
|
+
// Add to In Progress
|
|
229
|
+
if (inProgIdx !== -1) {
|
|
230
|
+
const bullet = `- [${id} — ${title}](${rel})`;
|
|
231
|
+
const sectionStart = inProgIdx + 1;
|
|
232
|
+
// Remove "No items" placeholder if present
|
|
233
|
+
let endIdx = lines.slice(sectionStart).findIndex((l) => l.startsWith('## '));
|
|
234
|
+
if (endIdx === -1)
|
|
235
|
+
endIdx = lines.length - sectionStart;
|
|
236
|
+
else
|
|
237
|
+
endIdx = sectionStart + endIdx;
|
|
238
|
+
for (let i = sectionStart; i < endIdx; i++) {
|
|
239
|
+
if (lines[i] && lines[i].includes('No items currently in progress')) {
|
|
240
|
+
lines.splice(i, 1);
|
|
241
|
+
endIdx--;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Check if already exists (idempotent)
|
|
246
|
+
if (!lines.slice(sectionStart, endIdx).some((l) => l.includes(rel))) {
|
|
247
|
+
lines.splice(sectionStart, 0, '', bullet);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
writeFileSync(statusPath, lines.join(STRING_LITERALS.NEWLINE), { encoding: 'utf-8' });
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Remove WU entry from section in status.md
|
|
255
|
+
*
|
|
256
|
+
* @private
|
|
257
|
+
* @param {string[]} lines - File lines
|
|
258
|
+
* @param {number} sectionIdx - Section header index
|
|
259
|
+
* @param {string} rel - Relative WU path
|
|
260
|
+
* @param {string} id - WU ID
|
|
261
|
+
*/
|
|
262
|
+
function removeFromSection(lines, sectionIdx, rel, id) {
|
|
263
|
+
if (sectionIdx === -1)
|
|
264
|
+
return;
|
|
265
|
+
let i = sectionIdx + 1;
|
|
266
|
+
while (i < lines.length) {
|
|
267
|
+
if (lines[i].startsWith('## '))
|
|
268
|
+
break;
|
|
269
|
+
if (lines[i].includes(rel) || lines[i].includes(`[${id}`)) {
|
|
270
|
+
lines.splice(i, 1);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Handle worktree removal after blocking
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
* @param {object} doc - WU document
|
|
281
|
+
* @param {string} [worktreeOverride] - Custom worktree path
|
|
282
|
+
* @param {object} gitAdapter - Git adapter
|
|
283
|
+
*/
|
|
284
|
+
function handleWorktreeRemoval(doc, worktreeOverride, gitAdapter) {
|
|
285
|
+
const wt = worktreeOverride || defaultWorktreeFrom(doc);
|
|
286
|
+
if (wt && existsSync(wt)) {
|
|
287
|
+
try {
|
|
288
|
+
gitAdapter.removeWorktree(wt);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
console.warn(`[wu-status-transition] Could not remove worktree ${wt}: ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (wt) {
|
|
295
|
+
console.warn('[wu-status-transition] Worktree path not found; skipping removal');
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
console.warn('[wu-status-transition] No worktree path specified; skipping removal');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Handle worktree creation after unblocking
|
|
303
|
+
*
|
|
304
|
+
* @private
|
|
305
|
+
* @param {object} doc - WU document
|
|
306
|
+
* @param {string} [worktreeOverride] - Custom worktree path
|
|
307
|
+
* @param {object} gitAdapter - Git adapter
|
|
308
|
+
*/
|
|
309
|
+
function handleWorktreeCreation(doc, worktreeOverride, gitAdapter) {
|
|
310
|
+
const worktreePath = worktreeOverride || defaultWorktreeFrom(doc);
|
|
311
|
+
const branchName = defaultBranchFrom(doc);
|
|
312
|
+
if (!branchName) {
|
|
313
|
+
console.warn('[wu-status-transition] Cannot derive branch name; skipping worktree creation');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!worktreePath) {
|
|
317
|
+
console.warn('[wu-status-transition] Worktree path required; skipping creation');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (existsSync(worktreePath)) {
|
|
321
|
+
console.warn(`[wu-status-transition] Worktree ${worktreePath} already exists; skipping creation`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
gitAdapter.run(`git fetch ${REMOTES.ORIGIN} ${BRANCHES.MAIN}`);
|
|
325
|
+
if (branchExists(branchName, gitAdapter)) {
|
|
326
|
+
gitAdapter.run(`git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branchName)}`);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
gitAdapter.run(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} ${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Derive default worktree path from WU document
|
|
334
|
+
*
|
|
335
|
+
* @private
|
|
336
|
+
* @param {object} doc - WU document
|
|
337
|
+
* @returns {string | null} Worktree path
|
|
338
|
+
*/
|
|
339
|
+
function defaultWorktreeFrom(doc) {
|
|
340
|
+
const lane = (doc.lane || '').toString();
|
|
341
|
+
const laneK = lane
|
|
342
|
+
.trim()
|
|
343
|
+
.toLowerCase()
|
|
344
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
345
|
+
.replace(/^-+|-+$/g, '');
|
|
346
|
+
const idK = (doc.id || '').toLowerCase();
|
|
347
|
+
if (!laneK || !idK)
|
|
348
|
+
return null;
|
|
349
|
+
return `worktrees/${laneK}-${idK}`;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Derive default branch name from WU document
|
|
353
|
+
*
|
|
354
|
+
* @private
|
|
355
|
+
* @param {object} doc - WU document
|
|
356
|
+
* @returns {string | null} Branch name
|
|
357
|
+
*/
|
|
358
|
+
function defaultBranchFrom(doc) {
|
|
359
|
+
const lane = (doc.lane || '').toString();
|
|
360
|
+
const laneK = toKebab(lane);
|
|
361
|
+
const idK = (doc.id || '').toLowerCase();
|
|
362
|
+
if (!laneK || !idK)
|
|
363
|
+
return null;
|
|
364
|
+
return `lane/${laneK}/${idK}`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check if git branch exists
|
|
368
|
+
*
|
|
369
|
+
* @private
|
|
370
|
+
* @param {string} branch - Branch name
|
|
371
|
+
* @param {object} gitAdapter - Git adapter
|
|
372
|
+
* @returns {boolean} True if branch exists
|
|
373
|
+
*/
|
|
374
|
+
function branchExists(branch, gitAdapter) {
|
|
375
|
+
try {
|
|
376
|
+
gitAdapter.run(`git rev-parse --verify ${JSON.stringify(branch)}`);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status.md Update Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized status.md update functions (extracted from wu-done.mjs)
|
|
5
|
+
* Refactored to use frontmatter-based section headings (no magic strings)
|
|
6
|
+
*
|
|
7
|
+
* Used by both main wu:done flow AND recovery mode (DRY principle)
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Remove WU from In Progress section (idempotent)
|
|
11
|
+
* Refactored from wu-done.mjs line 471 to use frontmatter headings
|
|
12
|
+
*
|
|
13
|
+
* @param {string} statusPath - Path to status.md
|
|
14
|
+
* @param {string} id - WU ID
|
|
15
|
+
*/
|
|
16
|
+
export declare function updateStatusRemoveInProgress(statusPath: any, id: any): void;
|
|
17
|
+
/**
|
|
18
|
+
* Add WU to Completed section (idempotent - checks for duplicates)
|
|
19
|
+
* Refactored from wu-done.mjs line 499 to use frontmatter headings
|
|
20
|
+
*
|
|
21
|
+
* @param {string} statusPath - Path to status.md
|
|
22
|
+
* @param {string} id - WU ID
|
|
23
|
+
* @param {string} title - WU title
|
|
24
|
+
*/
|
|
25
|
+
export declare function addToStatusCompleted(statusPath: any, id: any, title: any): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status.md Update Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized status.md update functions (extracted from wu-done.mjs)
|
|
5
|
+
* Refactored to use frontmatter-based section headings (no magic strings)
|
|
6
|
+
*
|
|
7
|
+
* Used by both main wu:done flow AND recovery mode (DRY principle)
|
|
8
|
+
*/
|
|
9
|
+
/* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { parseBacklogFrontmatter } from './backlog-parser.js';
|
|
12
|
+
import { getSectionHeadingsWithDefaults } from './section-headings.js';
|
|
13
|
+
import { todayISO } from './date-utils.js';
|
|
14
|
+
import { die, createError, ErrorCodes } from './error-handler.js';
|
|
15
|
+
import { STRING_LITERALS } from './wu-constants.js';
|
|
16
|
+
/**
|
|
17
|
+
* Remove WU from In Progress section (idempotent)
|
|
18
|
+
* Refactored from wu-done.mjs line 471 to use frontmatter headings
|
|
19
|
+
*
|
|
20
|
+
* @param {string} statusPath - Path to status.md
|
|
21
|
+
* @param {string} id - WU ID
|
|
22
|
+
*/
|
|
23
|
+
export function updateStatusRemoveInProgress(statusPath, id) {
|
|
24
|
+
if (!existsSync(statusPath)) {
|
|
25
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Status file not found: ${statusPath}`, {
|
|
26
|
+
path: statusPath,
|
|
27
|
+
function: 'updateStatusRemoveInProgress',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Use frontmatter parser to get configured section headings
|
|
31
|
+
const { frontmatter, markdown } = parseBacklogFrontmatter(statusPath);
|
|
32
|
+
const headings = getSectionHeadingsWithDefaults(frontmatter, 'status');
|
|
33
|
+
const rel = `wu/${id}.yaml`;
|
|
34
|
+
const lines = markdown.split(STRING_LITERALS.NEWLINE);
|
|
35
|
+
// Find In Progress section using configured heading
|
|
36
|
+
const startIdx = lines.findIndex((l) => l.trim() === headings.in_progress);
|
|
37
|
+
if (startIdx === -1) {
|
|
38
|
+
throw createError(ErrorCodes.SECTION_NOT_FOUND, `Could not find "${headings.in_progress}" section in ${statusPath}`, { path: statusPath, section: headings.in_progress, function: 'updateStatusRemoveInProgress' });
|
|
39
|
+
}
|
|
40
|
+
// Find section boundaries
|
|
41
|
+
let endIdx = lines.slice(startIdx + 1).findIndex((l) => l.startsWith('## '));
|
|
42
|
+
endIdx = endIdx === -1 ? lines.length - startIdx - 1 : startIdx + 1 + endIdx;
|
|
43
|
+
// Remove WU entry (idempotent - safe to call if already removed)
|
|
44
|
+
let removed = false;
|
|
45
|
+
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
46
|
+
if (lines[i] && (lines[i].includes(rel) || lines[i].includes(`[${id}`))) {
|
|
47
|
+
lines.splice(i, 1);
|
|
48
|
+
removed = true;
|
|
49
|
+
endIdx--;
|
|
50
|
+
i--; // Adjust index after splice
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Add placeholder if section is now empty
|
|
54
|
+
if (removed) {
|
|
55
|
+
const section = lines.slice(startIdx + 1, endIdx).filter((l) => l.trim() !== '');
|
|
56
|
+
if (section.length === 0) {
|
|
57
|
+
lines.splice(endIdx, 0, '', '(No items currently in progress)', '');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Reconstruct file with frontmatter preservation
|
|
61
|
+
const raw = readFileSync(statusPath, { encoding: 'utf-8' });
|
|
62
|
+
const frontmatterMatch = raw.match(/^---\n[\s\S]*?\n---\n/);
|
|
63
|
+
const frontmatterText = frontmatterMatch ? frontmatterMatch[0] : '';
|
|
64
|
+
writeFileSync(statusPath, frontmatterText + lines.join(STRING_LITERALS.NEWLINE), {
|
|
65
|
+
encoding: 'utf-8',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add WU to Completed section (idempotent - checks for duplicates)
|
|
70
|
+
* Refactored from wu-done.mjs line 499 to use frontmatter headings
|
|
71
|
+
*
|
|
72
|
+
* @param {string} statusPath - Path to status.md
|
|
73
|
+
* @param {string} id - WU ID
|
|
74
|
+
* @param {string} title - WU title
|
|
75
|
+
*/
|
|
76
|
+
export function addToStatusCompleted(statusPath, id, title) {
|
|
77
|
+
if (!existsSync(statusPath)) {
|
|
78
|
+
throw createError(ErrorCodes.FILE_NOT_FOUND, `Status file not found: ${statusPath}`, {
|
|
79
|
+
path: statusPath,
|
|
80
|
+
function: 'addToStatusCompleted',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Use frontmatter parser to get configured section headings
|
|
84
|
+
const { frontmatter, markdown } = parseBacklogFrontmatter(statusPath);
|
|
85
|
+
const headings = getSectionHeadingsWithDefaults(frontmatter, 'status');
|
|
86
|
+
const rel = `wu/${id}.yaml`;
|
|
87
|
+
const date = todayISO();
|
|
88
|
+
const completedEntry = `- [${id} — ${title}](${rel}) — ${date}`;
|
|
89
|
+
const lines = markdown.split(STRING_LITERALS.NEWLINE);
|
|
90
|
+
// Find Completed section using configured heading
|
|
91
|
+
const completedIdx = lines.findIndex((l) => l.trim() === headings.completed);
|
|
92
|
+
if (completedIdx === -1) {
|
|
93
|
+
die(`Could not find "${headings.completed}" section in ${statusPath}`);
|
|
94
|
+
}
|
|
95
|
+
// Idempotent check: skip if already in Completed section
|
|
96
|
+
const nextSectionIdx = lines.slice(completedIdx + 1).findIndex((l) => l.startsWith('## '));
|
|
97
|
+
const completedEndIdx = nextSectionIdx === -1 ? lines.length : completedIdx + 1 + nextSectionIdx;
|
|
98
|
+
const completedSection = lines.slice(completedIdx, completedEndIdx).join(STRING_LITERALS.NEWLINE);
|
|
99
|
+
if (completedSection.includes(`[${id}`)) {
|
|
100
|
+
console.log(`[wu-status-updater] ${id} already in Completed section (idempotent skip)`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Insert at top of Completed section (after header, skipping empty lines)
|
|
104
|
+
let insertIdx = completedIdx + 1;
|
|
105
|
+
while (insertIdx < lines.length && lines[insertIdx].trim() === '') {
|
|
106
|
+
insertIdx++;
|
|
107
|
+
}
|
|
108
|
+
lines.splice(insertIdx, 0, completedEntry);
|
|
109
|
+
// Reconstruct file with frontmatter preservation
|
|
110
|
+
const raw = readFileSync(statusPath, { encoding: 'utf-8' });
|
|
111
|
+
const frontmatterMatch = raw.match(/^---\n[\s\S]*?\n---\n/);
|
|
112
|
+
const frontmatterText = frontmatterMatch ? frontmatterMatch[0] : '';
|
|
113
|
+
writeFileSync(statusPath, frontmatterText + lines.join(STRING_LITERALS.NEWLINE), {
|
|
114
|
+
encoding: 'utf-8',
|
|
115
|
+
});
|
|
116
|
+
}
|