@opengsd/gsd-core 1.7.0-rc.6 → 1.7.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/bin/install.js +5 -4
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +57 -57
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +27 -13
- package/gsd-core/bin/lib/install-engine.cjs +3 -2
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -6
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +2 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +25 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +2 -2
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/gsd-statusline.js +164 -14
- package/package.json +4 -2
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -267,7 +267,7 @@ function normalizeNodePath(execPath, opts) {
|
|
|
267
267
|
return execPath;
|
|
268
268
|
const env = (opts && opts.env) || process.env;
|
|
269
269
|
const existsSync = (opts && opts.existsSync) || node_fs_1.default.existsSync;
|
|
270
|
-
const normalizedForMatch =
|
|
270
|
+
const normalizedForMatch = shellCmdProjection.posixNormalize(execPath);
|
|
271
271
|
if (/\/fnm_multishells\/[0-9]+_[0-9]+\/node(\.exe)?$/i.test(normalizedForMatch)) {
|
|
272
272
|
const candidates = [];
|
|
273
273
|
if (env.FNM_DIR) {
|
|
@@ -283,11 +283,16 @@ function normalizeNodePath(execPath, opts) {
|
|
|
283
283
|
}
|
|
284
284
|
return execPath;
|
|
285
285
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
// Homebrew (macOS Intel /usr/local, Apple Silicon /opt/homebrew, Linuxbrew
|
|
287
|
+
// /home/linuxbrew/.linuxbrew, and any custom HOMEBREW_PREFIX) pins node at
|
|
288
|
+
// <prefix>/Cellar/node(<@ver>)?/<ver>/bin/node, then deletes prior versions on
|
|
289
|
+
// `brew upgrade node`. Rewrite to the stable <prefix>/bin/node symlink, which
|
|
290
|
+
// survives the upgrade. Derive <prefix> from the path itself (more reliable
|
|
291
|
+
// than HOMEBREW_PREFIX env — the path IS the install location) so every layout
|
|
292
|
+
// is covered by one branch instead of one per known prefix (#2185).
|
|
293
|
+
const homebrewMatch = normalizedForMatch.match(/^(.+)\/Cellar\/node(@\d+)?\/[^/]+\/bin\/node(\.exe)?$/i);
|
|
294
|
+
if (homebrewMatch) {
|
|
295
|
+
return `${homebrewMatch[1]}/bin/node${homebrewMatch[3] || ''}`;
|
|
291
296
|
}
|
|
292
297
|
// mise pins a concrete node version at <data>/installs/node/<ver>/bin/node
|
|
293
298
|
// (Windows: <data>/installs/node/<ver>/node.exe). Node realpaths
|
|
@@ -311,7 +316,7 @@ function resolveNodeRunner(opts) {
|
|
|
311
316
|
if (!execPath)
|
|
312
317
|
return null;
|
|
313
318
|
const stablePath = normalizeNodePath(execPath, opts);
|
|
314
|
-
return JSON.stringify(
|
|
319
|
+
return JSON.stringify(shellCmdProjection.posixNormalize(stablePath));
|
|
315
320
|
}
|
|
316
321
|
function resolveBashRunner(opts) {
|
|
317
322
|
const platform = (opts && opts.platform) || process.platform;
|
|
@@ -332,7 +337,7 @@ function resolveBashRunner(opts) {
|
|
|
332
337
|
}
|
|
333
338
|
for (const candidate of candidates) {
|
|
334
339
|
if (candidate && exists(candidate)) {
|
|
335
|
-
return JSON.stringify(
|
|
340
|
+
return JSON.stringify(shellCmdProjection.posixNormalize(candidate));
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
return null;
|
|
@@ -372,7 +377,7 @@ function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
|
372
377
|
}
|
|
373
378
|
else {
|
|
374
379
|
_runnerToken = m[1];
|
|
375
|
-
const runnerPath = (m[2] || m[3] || m[4] || '')
|
|
380
|
+
const runnerPath = shellCmdProjection.posixNormalize(m[2] || m[3] || m[4] || '');
|
|
376
381
|
const stableRunner = normalizeNodePath(runnerPath);
|
|
377
382
|
if (stableRunner === runnerPath && platform !== 'win32')
|
|
378
383
|
continue;
|
|
@@ -562,10 +567,10 @@ function buildCodexHookWindowsShimIR(scriptAbsPath, absoluteRunnerToken) {
|
|
|
562
567
|
catch {
|
|
563
568
|
interpreter = absoluteRunnerToken;
|
|
564
569
|
}
|
|
565
|
-
const targetAbs =
|
|
570
|
+
const targetAbs = shellCmdProjection.posixNormalize(scriptAbsPath);
|
|
566
571
|
const scriptQuoted = JSON.stringify(targetAbs);
|
|
567
572
|
const cmdPath = scriptAbsPath.replace(/\.js$/, '.cmd');
|
|
568
|
-
const hookCommand = JSON.stringify(
|
|
573
|
+
const hookCommand = JSON.stringify(shellCmdProjection.posixNormalize(cmdPath));
|
|
569
574
|
const runnerQuoted = JSON.stringify(interpreter);
|
|
570
575
|
return {
|
|
571
576
|
invocation: { interpreter, target: scriptAbsPath },
|
|
@@ -584,7 +589,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
584
589
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
585
590
|
if (!absoluteRunner)
|
|
586
591
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
587
|
-
const scriptPath = node_path_1.default.resolve(targetDir, 'hooks', 'gsd-check-update.js')
|
|
592
|
+
const scriptPath = shellCmdProjection.posixNormalize(node_path_1.default.resolve(targetDir, 'hooks', 'gsd-check-update.js'));
|
|
588
593
|
const cmdShimPath = scriptPath.replace(/\.js$/, '.cmd');
|
|
589
594
|
let managedCommand;
|
|
590
595
|
if (platform === 'win32') {
|
|
@@ -614,7 +619,7 @@ function ensureCodexHooksJsonSessionStart(targetDir, opts = {}) {
|
|
|
614
619
|
if (!managedCommand)
|
|
615
620
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
616
621
|
const commandWindows = platform === 'win32'
|
|
617
|
-
? JSON.stringify(
|
|
622
|
+
? JSON.stringify(shellCmdProjection.posixNormalize(cmdShimPath))
|
|
618
623
|
: undefined;
|
|
619
624
|
return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows });
|
|
620
625
|
}
|
|
@@ -624,7 +629,7 @@ function ensureCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
|
624
629
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
625
630
|
if (!absoluteRunner)
|
|
626
631
|
return { changed: false, wrote: false, path: hooksJsonPath };
|
|
627
|
-
const scriptPath = node_path_1.default.resolve(targetDir, 'hooks', 'gsd-context-monitor.js')
|
|
632
|
+
const scriptPath = shellCmdProjection.posixNormalize(node_path_1.default.resolve(targetDir, 'hooks', 'gsd-context-monitor.js'));
|
|
628
633
|
let managedCommand;
|
|
629
634
|
if (platform === 'win32') {
|
|
630
635
|
const shimIR = buildCodexHookWindowsShimIR(scriptPath, absoluteRunner);
|
|
@@ -667,6 +672,7 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
667
672
|
opts = {};
|
|
668
673
|
const platform = opts.platform || process.platform;
|
|
669
674
|
const runtime = opts.runtime || 'generic';
|
|
675
|
+
const hookShell = opts.hookShell;
|
|
670
676
|
const isShellHook = hookName.endsWith('.sh');
|
|
671
677
|
if (shellHookOmitsBashRunner({ platform, runtime, isShellHook })) {
|
|
672
678
|
if (opts.portableHooks) {
|
|
@@ -676,7 +682,7 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
676
682
|
});
|
|
677
683
|
return JSON.stringify(`${portableBaseDir}/hooks/${hookName}`);
|
|
678
684
|
}
|
|
679
|
-
return JSON.stringify(
|
|
685
|
+
return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName);
|
|
680
686
|
}
|
|
681
687
|
const nodeRunner = resolveNodeRunner();
|
|
682
688
|
const runner = isShellHook ? resolveBashRunner(opts) : nodeRunner;
|
|
@@ -692,14 +698,16 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
692
698
|
scriptPath: `${portableBaseDir}/hooks/${hookName}`,
|
|
693
699
|
runtime: opts.runtime || 'generic',
|
|
694
700
|
platform,
|
|
701
|
+
hookShell,
|
|
695
702
|
});
|
|
696
703
|
}
|
|
697
|
-
const hooksPath =
|
|
704
|
+
const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
698
705
|
return projectManagedHookCommand({
|
|
699
706
|
absoluteRunner: runner,
|
|
700
707
|
scriptPath: hooksPath,
|
|
701
708
|
runtime,
|
|
702
709
|
platform,
|
|
710
|
+
hookShell,
|
|
703
711
|
});
|
|
704
712
|
}
|
|
705
713
|
// ---------------------------------------------------------------------------
|
|
@@ -844,7 +852,7 @@ function writeClineArtifacts(targetDir, isGlobalInstall) {
|
|
|
844
852
|
function buildCursorHookEntry(scriptPath) {
|
|
845
853
|
return {
|
|
846
854
|
type: 'command',
|
|
847
|
-
command:
|
|
855
|
+
command: shellCmdProjection.posixNormalize(scriptPath),
|
|
848
856
|
[GSD_CURSOR_HOOK_MARKER]: true,
|
|
849
857
|
};
|
|
850
858
|
}
|
|
@@ -13,6 +13,7 @@ exports.ORM_INFO = exports.SCHEMA_PATTERNS = void 0;
|
|
|
13
13
|
exports.detectSchemaFiles = detectSchemaFiles;
|
|
14
14
|
exports.detectSchemaOrm = detectSchemaOrm;
|
|
15
15
|
exports.checkSchemaDrift = checkSchemaDrift;
|
|
16
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
17
|
exports.SCHEMA_PATTERNS = [
|
|
17
18
|
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
|
|
18
19
|
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
|
|
@@ -61,7 +62,7 @@ function detectSchemaFiles(files) {
|
|
|
61
62
|
const matches = [];
|
|
62
63
|
const orms = new Set();
|
|
63
64
|
for (const rawFile of files) {
|
|
64
|
-
const file =
|
|
65
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
|
|
65
66
|
for (const { pattern, orm } of exports.SCHEMA_PATTERNS) {
|
|
66
67
|
if (pattern.test(file)) {
|
|
67
68
|
matches.push(rawFile);
|
|
@@ -341,7 +341,7 @@ function sanitizeForDisplay(text) {
|
|
|
341
341
|
let sanitized = sanitizeForPrompt(text);
|
|
342
342
|
const protocolLeakPatterns = [
|
|
343
343
|
/^\s*(?:assistant|user|system)\s+to=[^:\s]+:[^\n]+$/i,
|
|
344
|
-
/^\s*<\|(?:assistant|user|system)[^|]*\|>\s*$/i,
|
|
344
|
+
/^\s*<\|(?:assistant|user|system)[^|]*\|>\s*$/i, // allow-adhoc-markdown: not a GFM table-cell scan — matches `<|role|>` protocol-leak marker tokens (prompt-injection sanitization), a false-positive on the table-regex pipe+cell-class fingerprint
|
|
345
345
|
];
|
|
346
346
|
sanitized = sanitized
|
|
347
347
|
.split('\n')
|
|
@@ -15,6 +15,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.toPosixPath = toPosixPath;
|
|
19
|
+
exports.toNativePath = toNativePath;
|
|
20
|
+
exports.posixNormalize = posixNormalize;
|
|
18
21
|
exports.hookCommandNeedsPowerShellCallOperator = hookCommandNeedsPowerShellCallOperator;
|
|
19
22
|
exports.formatHookCommandForRuntime = formatHookCommandForRuntime;
|
|
20
23
|
exports.shellHookOmitsBashRunner = shellHookOmitsBashRunner;
|
|
@@ -52,26 +55,58 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
52
55
|
// can intercept calls from this seam — destructured imports capture references
|
|
53
56
|
// at load time and become un-mockable.
|
|
54
57
|
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
58
|
+
/**
|
|
59
|
+
* Convert a filesystem path to POSIX form (forward slashes) by translating the
|
|
60
|
+
* platform-native separator. Single seam for native→POSIX conversion.
|
|
61
|
+
*
|
|
62
|
+
* Prefer this over `p.replace(/\\/g, '/')`: the regex form hardcodes both
|
|
63
|
+
* separators and corrupts POSIX paths containing a literal backslash (a legal
|
|
64
|
+
* filename character). Splitting on `path.sep` only ever touches real
|
|
65
|
+
* separators — a no-op on POSIX, `\`→`/` on Windows.
|
|
66
|
+
*/
|
|
67
|
+
function toPosixPath(p) {
|
|
68
|
+
return p.split(node_path_1.default.sep).join(node_path_1.default.posix.sep);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert a filesystem path to the platform-native separator form. No-op on
|
|
72
|
+
* POSIX; `/`→`\` on Windows. Prefer this over a
|
|
73
|
+
* `process.platform === 'win32' ? p.replace(/\//g, '\\') : p` ternary.
|
|
74
|
+
*/
|
|
75
|
+
function toNativePath(p) {
|
|
76
|
+
return p.split(node_path_1.default.posix.sep).join(node_path_1.default.sep);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Normalize ALL backslashes to forward slashes, unconditionally and independent
|
|
80
|
+
* of the running OS. Use this when emitting a path into a POSIX/bash target
|
|
81
|
+
* (which may differ from the running platform — e.g. generating a Windows config
|
|
82
|
+
* on a Linux runner) or when parsing input whose separators are unpredictable.
|
|
83
|
+
*
|
|
84
|
+
* Contrast `toPosixPath`, which is running-OS-relative (splits on `path.sep`) and
|
|
85
|
+
* is for *this machine's* filesystem paths. Do NOT use `toPosixPath` for
|
|
86
|
+
* target-platform projection — on a Linux runner it would not convert a
|
|
87
|
+
* Windows-target path's backslashes.
|
|
88
|
+
*/
|
|
89
|
+
function posixNormalize(p) {
|
|
90
|
+
return p.replace(/\\/g, '/');
|
|
91
|
+
}
|
|
55
92
|
/**
|
|
56
93
|
* Return true when a managed hook command must be prefixed with PowerShell's
|
|
57
94
|
* call operator so a quoted executable token is invokable by the target
|
|
58
95
|
* runtime/shell combination.
|
|
59
96
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* one-line re-enable, per the conservative policy below. Note: Antigravity —
|
|
68
|
-
* the Gemini-backend successor — never matched the old `runtime === 'gemini'`
|
|
69
|
-
* check, so its behavior (no prefix) is unchanged.
|
|
97
|
+
* The `&`/no-`&` decision is keyed on the **effective hook-execution shell**
|
|
98
|
+
* (`opts.hookShell`), not on runtime alone — a single runtime (Claude Code)
|
|
99
|
+
* can host either Git Bash or PowerShell on Windows, and no single static
|
|
100
|
+
* command string is valid in both (#2236):
|
|
101
|
+
* - Git Bash: `"node.exe" "hook.js"` works; `& "node.exe" …` → syntax error.
|
|
102
|
+
* - PowerShell: `& "node.exe" "hook.js"` works; bare `"node.exe" …` →
|
|
103
|
+
* `Unexpected token`.
|
|
70
104
|
*
|
|
71
|
-
*
|
|
105
|
+
* Default is `false` (Git Bash form) for backward compatibility. Set
|
|
106
|
+
* `opts.hookShell = 'powershell'` to emit the PowerShell call-operator form.
|
|
72
107
|
*/
|
|
73
|
-
function hookCommandNeedsPowerShellCallOperator(
|
|
74
|
-
return
|
|
108
|
+
function hookCommandNeedsPowerShellCallOperator(opts = {}) {
|
|
109
|
+
return opts.hookShell === 'powershell';
|
|
75
110
|
}
|
|
76
111
|
/**
|
|
77
112
|
* Project a fully-assembled hook command string for the target runtime.
|
|
@@ -122,7 +157,7 @@ function formatManagedHookScriptToken(scriptPath, opts = {}) {
|
|
|
122
157
|
const platform = opts.platform || process.platform;
|
|
123
158
|
if (platform !== 'win32')
|
|
124
159
|
return null;
|
|
125
|
-
return JSON.stringify(scriptPath
|
|
160
|
+
return JSON.stringify(posixNormalize(scriptPath));
|
|
126
161
|
}
|
|
127
162
|
function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, hookPathStyle }) {
|
|
128
163
|
if (!dirName)
|
|
@@ -140,29 +175,30 @@ function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, hookPat
|
|
|
140
175
|
: `"$CLAUDE_PROJECT_DIR"/${dirName}`;
|
|
141
176
|
}
|
|
142
177
|
function projectPortableHookBaseDir({ configDir, homeDir }) {
|
|
143
|
-
const normalizedConfigDir = String(configDir || '')
|
|
144
|
-
const normalizedHome = String(homeDir || '')
|
|
178
|
+
const normalizedConfigDir = posixNormalize(String(configDir || ''));
|
|
179
|
+
const normalizedHome = posixNormalize(String(homeDir || ''));
|
|
145
180
|
if (!normalizedConfigDir || !normalizedHome)
|
|
146
181
|
return normalizedConfigDir;
|
|
147
182
|
return normalizedConfigDir.startsWith(normalizedHome)
|
|
148
183
|
? '$HOME' + normalizedConfigDir.slice(normalizedHome.length)
|
|
149
184
|
: normalizedConfigDir;
|
|
150
185
|
}
|
|
151
|
-
function projectShellCommandText({ runnerToken, argTokens = [], runtime = 'generic', platform = process.platform, }) {
|
|
186
|
+
function projectShellCommandText({ runnerToken, argTokens = [], runtime = 'generic', platform = process.platform, hookShell, }) {
|
|
152
187
|
if (!runnerToken)
|
|
153
188
|
return null;
|
|
154
189
|
const parts = [runnerToken, ...argTokens.filter(Boolean)];
|
|
155
|
-
return formatHookCommandForRuntime(parts.join(' '), { platform, runtime });
|
|
190
|
+
return formatHookCommandForRuntime(parts.join(' '), { platform, runtime, hookShell });
|
|
156
191
|
}
|
|
157
|
-
function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'generic', platform = process.platform }) {
|
|
192
|
+
function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'generic', platform = process.platform, hookShell }) {
|
|
158
193
|
if (!absoluteRunner || !scriptPath)
|
|
159
194
|
return null;
|
|
160
|
-
const normalizedScriptPath = platform === 'win32' ? scriptPath
|
|
195
|
+
const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath;
|
|
161
196
|
return projectShellCommandText({
|
|
162
197
|
runnerToken: absoluteRunner,
|
|
163
198
|
argTokens: [JSON.stringify(normalizedScriptPath)],
|
|
164
199
|
runtime,
|
|
165
200
|
platform,
|
|
201
|
+
hookShell,
|
|
166
202
|
});
|
|
167
203
|
}
|
|
168
204
|
const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
@@ -255,14 +291,14 @@ function isManagedHookCommand(commandText, opts = {}) {
|
|
|
255
291
|
for (const arg of opts.args) {
|
|
256
292
|
if (typeof arg !== 'string')
|
|
257
293
|
continue;
|
|
258
|
-
const argBasename = arg
|
|
294
|
+
const argBasename = posixNormalize(arg).split('/').pop() || '';
|
|
259
295
|
if (isManagedHookBasename(argBasename, { surface }))
|
|
260
296
|
return true;
|
|
261
297
|
}
|
|
262
298
|
}
|
|
263
|
-
const normalizedCommand = commandText
|
|
299
|
+
const normalizedCommand = posixNormalize(commandText);
|
|
264
300
|
if (typeof opts.configDir === 'string' && opts.configDir.length > 0) {
|
|
265
|
-
const normalizedHooksDir = `${node_path_1.default.join(opts.configDir, 'hooks')
|
|
301
|
+
const normalizedHooksDir = `${posixNormalize(node_path_1.default.join(opts.configDir, 'hooks'))}/`;
|
|
266
302
|
if (!normalizedCommand.includes(normalizedHooksDir))
|
|
267
303
|
return false;
|
|
268
304
|
}
|
|
@@ -292,7 +328,7 @@ const ANCHORED_HOOK_SCRIPT_TOKEN = /^"\$[A-Za-z_][A-Za-z0-9_]*"\//;
|
|
|
292
328
|
function projectLegacySettingsHookCommand({ absoluteRunner, scriptPath, scriptToken, runtime = 'generic', platform = process.platform, }) {
|
|
293
329
|
if (!absoluteRunner || !scriptPath)
|
|
294
330
|
return null;
|
|
295
|
-
const normalizedScriptPath = platform === 'win32' ? scriptPath
|
|
331
|
+
const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath;
|
|
296
332
|
// #1693: a script path already carrying a `"$CLAUDE_PROJECT_DIR"`-anchored
|
|
297
333
|
// quoted prefix (local installs) is already a valid shell token — only the
|
|
298
334
|
// variable is quoted, the rest is bare. JSON.stringify-ing it on Windows
|
|
@@ -350,7 +386,7 @@ function projectPathActionProjection({ mode = 'repair', targetDir, platform = pr
|
|
|
350
386
|
let shellActions;
|
|
351
387
|
if (isWin32) {
|
|
352
388
|
const psTargetDir = escapePowerShellSingleQuoted(targetDir);
|
|
353
|
-
const bashTargetDir = escapeSingleQuotedShellLiteral(String(targetDir)
|
|
389
|
+
const bashTargetDir = escapeSingleQuotedShellLiteral(posixNormalize(String(targetDir)));
|
|
354
390
|
shellActions = [
|
|
355
391
|
{
|
|
356
392
|
label: 'PowerShell',
|
|
@@ -42,6 +42,7 @@ exports.runSmartEntry = runSmartEntry;
|
|
|
42
42
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
45
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
47
|
const ioMod = require("./io.cjs");
|
|
47
48
|
const { output } = ioMod;
|
|
@@ -274,9 +275,9 @@ function detectSignals(cwd, now = Date.now) {
|
|
|
274
275
|
fmScalar(fm, body, 'progress', 'Progress');
|
|
275
276
|
// Blockers list: `- <text>` items under a `## Blockers` heading.
|
|
276
277
|
const blockers = [];
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
279
|
-
const items =
|
|
278
|
+
const blockersSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => h.level === 2 && h.text.trim().toLowerCase() === 'blockers', { levelBounded: true });
|
|
279
|
+
if (blockersSection) {
|
|
280
|
+
const items = blockersSection.body.match(/^-\s+(.+)$/gm) || [];
|
|
280
281
|
for (const item of items)
|
|
281
282
|
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
282
283
|
}
|
|
@@ -28,7 +28,7 @@ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
|
28
28
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
29
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
30
|
const phaseIdMod = require("./phase-id.cjs");
|
|
31
|
-
const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
|
|
31
|
+
const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter } = frontmatter;
|
|
32
32
|
const { escapeRegex } = phaseIdMod;
|
|
33
33
|
// Stop predicate for section-body slicing: a level-2+ heading ends the section.
|
|
34
34
|
const STOP_H2_PLUS = (lv) => lv >= 2;
|
|
@@ -226,7 +226,7 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
226
226
|
const reassemble = (b) => hasFrontmatter
|
|
227
227
|
? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}`
|
|
228
228
|
: b;
|
|
229
|
-
const today = deps.clock.
|
|
229
|
+
const today = deps.clock.localToday();
|
|
230
230
|
// Consult the field-classification table for the frontmatter keys this
|
|
231
231
|
// transition touches (codex Phase 1 review: "table not consulted by
|
|
232
232
|
// transitionCore"). The table tracks FRONTMATTER keys (lowercase: `status`,
|
|
@@ -284,8 +284,8 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
284
284
|
updated.push('Current focus');
|
|
285
285
|
}
|
|
286
286
|
// ## Current Position section mutation (#1104, #1365).
|
|
287
|
-
//
|
|
288
|
-
//
|
|
287
|
+
// `locateCurrentPosition` (fence-aware, tokenizeHeadings-based) locates
|
|
288
|
+
// the section; mirrors state.cts:2261-2324 byte-for-behaviour.
|
|
289
289
|
body = mutateCurrentPositionFirstTime(body, intent, today, updated);
|
|
290
290
|
}
|
|
291
291
|
else {
|
|
@@ -338,6 +338,20 @@ function sliceCurrentPositionSection(body) {
|
|
|
338
338
|
* First-time ## Current Position mutation: update Phase / Plan / Status /
|
|
339
339
|
* Last activity lines. Mirrors state.cts:2261-2324 byte-for-behaviour
|
|
340
340
|
* (inline regex first, pipe-table fallback via stateReplaceField — #1257).
|
|
341
|
+
*
|
|
342
|
+
* F2 (#2245 review, MAJOR): a prior revision of this function used
|
|
343
|
+
* `collectSection`/`replaceSection` here, whose default `levelBounded: true`
|
|
344
|
+
* only stops the section at the next heading of level <= the opener's own
|
|
345
|
+
* level (H1/H2 for a `##`-opened section) — an H3+ subsection nested under
|
|
346
|
+
* `## Current Position` was NOT a stop boundary and got folded into
|
|
347
|
+
* `sectionBody`, so the field regexes below (which run with the `m` flag,
|
|
348
|
+
* matching ANY line start in the body) could clobber a same-named line
|
|
349
|
+
* inside that subsection (the #2130/#2067/#2080 truncation/clobber class).
|
|
350
|
+
* Restored to the fence-aware `locateCurrentPosition` locator (which stops
|
|
351
|
+
* at ANY heading level >= 2, `STOP_H2_PLUS` — H2 through H6) + manual splice,
|
|
352
|
+
* exactly matching the `mutateCurrentPositionResume`/
|
|
353
|
+
* `mutateCurrentPositionForAdvance` siblings below, both of which use
|
|
354
|
+
* `locateCurrentPosition` directly.
|
|
341
355
|
*/
|
|
342
356
|
function mutateCurrentPositionFirstTime(body, intent, today, updated) {
|
|
343
357
|
const span = locateCurrentPosition(body);
|
|
@@ -414,25 +428,6 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
|
|
|
414
428
|
}
|
|
415
429
|
return body.slice(0, span.start) + sectionBody + body.slice(span.end);
|
|
416
430
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Strip ALL frontmatter blocks from the start of `content`.
|
|
419
|
-
*
|
|
420
|
-
* TODO (ADR-1769 follow-up): move to `frontmatter.cjs` or `state-document.cjs`
|
|
421
|
-
* so it's a shared primitive. Inlined here in Phase 1 to avoid touching
|
|
422
|
-
* `state.cjs` (which is the migration target itself) and to keep the Phase 1
|
|
423
|
-
* diff contained. Body is byte-identical to `state.cts:1653 stripFrontmatter`
|
|
424
|
-
* (same CRLF + stacked-block handling).
|
|
425
|
-
*/
|
|
426
|
-
function stripFrontmatter(content) {
|
|
427
|
-
let result = content;
|
|
428
|
-
while (true) {
|
|
429
|
-
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
430
|
-
if (stripped === result)
|
|
431
|
-
break;
|
|
432
|
-
result = stripped;
|
|
433
|
-
}
|
|
434
|
-
return result;
|
|
435
|
-
}
|
|
436
431
|
/**
|
|
437
432
|
* Update fields within the ## Current Position section for advancePlan.
|
|
438
433
|
* Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
|
|
@@ -502,7 +497,7 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
|
|
|
502
497
|
* adapter to construct CLI output.
|
|
503
498
|
*/
|
|
504
499
|
function advancePlanCore(content, deps) {
|
|
505
|
-
const today = deps.clock.
|
|
500
|
+
const today = deps.clock.localToday();
|
|
506
501
|
// #1255: body-field replacements operate on body only (frontmatter stripped),
|
|
507
502
|
// not on the full content. The YAML `status:` key matches `^Status:\s*`
|
|
508
503
|
// before the body field if full content is passed (codex Phase 2 review:
|
|
@@ -591,8 +586,8 @@ function advancePlanCore(content, deps) {
|
|
|
591
586
|
* Migrates the inline STATE.md transform that lived inside `cmdPhaseComplete`
|
|
592
587
|
* (phase.cts) onto the substrate. Owns the field-classification-governed body
|
|
593
588
|
* mutations: Current Phase (preserving the `of total` shape and phase name),
|
|
594
|
-
* Current Phase Name, Status (`
|
|
595
|
-
* `Ready to plan`), Current Plan (`Not started`), Last Activity + Description,
|
|
589
|
+
* Current Phase Name, Status (`All phases complete` on the last phase, else
|
|
590
|
+
* `Ready to plan` per ADR-2207), Current Plan (`Not started`), Last Activity + Description,
|
|
596
591
|
* and the Completed/Total Phases + Progress percent block (re-derived from the
|
|
597
592
|
* roadmap via the injected `roadmapProvider`).
|
|
598
593
|
*
|
|
@@ -608,7 +603,7 @@ function advancePlanCore(content, deps) {
|
|
|
608
603
|
*/
|
|
609
604
|
function completePhaseCore(content, intent, deps) {
|
|
610
605
|
const updated = [];
|
|
611
|
-
const today = deps.clock.
|
|
606
|
+
const today = deps.clock.localToday();
|
|
612
607
|
// Consult the field-classification table for the frontmatter keys this
|
|
613
608
|
// transition touches (same guard beginPhaseCore applies). A missing row is a
|
|
614
609
|
// substrate defect — fail loudly rather than silently re-encoding policy.
|
|
@@ -672,8 +667,10 @@ function completePhaseCore(content, intent, deps) {
|
|
|
672
667
|
updated.push('Current Phase Name');
|
|
673
668
|
}
|
|
674
669
|
}
|
|
675
|
-
// Status — `
|
|
676
|
-
|
|
670
|
+
// Status — `All phases complete` on the final phase (ADR-2207), otherwise
|
|
671
|
+
// `Ready to plan`. Milestone termination (`<version> milestone complete`) is
|
|
672
|
+
// owned solely by the milestone-close verb (milestoneCompleteCore).
|
|
673
|
+
const statusValue = intent.isLastPhase ? 'All phases complete' : 'Ready to plan';
|
|
677
674
|
const statusAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Status', null, statusValue);
|
|
678
675
|
if (statusAfter !== body) {
|
|
679
676
|
body = statusAfter;
|
|
@@ -762,7 +759,7 @@ function completePhaseCore(content, intent, deps) {
|
|
|
762
759
|
*/
|
|
763
760
|
function plannedPhaseCore(content, intent, deps) {
|
|
764
761
|
const updated = [];
|
|
765
|
-
const today = deps.clock.
|
|
762
|
+
const today = deps.clock.localToday();
|
|
766
763
|
for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
|
|
767
764
|
const cls = getFieldClassification(fmKey);
|
|
768
765
|
if (cls === null) {
|
|
@@ -840,7 +837,7 @@ function plannedPhaseCore(content, intent, deps) {
|
|
|
840
837
|
* directly and must not run the steady-state `syncStateFrontmatter` post-sync.
|
|
841
838
|
*/
|
|
842
839
|
function milestoneSwitchCore(content, intent, deps) {
|
|
843
|
-
const today = deps.clock.
|
|
840
|
+
const today = deps.clock.localToday();
|
|
844
841
|
const updated = [
|
|
845
842
|
'milestone',
|
|
846
843
|
'milestone_name',
|
|
@@ -903,6 +900,70 @@ function milestoneSwitchCore(content, intent, deps) {
|
|
|
903
900
|
// ----------------------------------------------------------------------------
|
|
904
901
|
// milestoneComplete — intent implementation (Phase 5)
|
|
905
902
|
// ----------------------------------------------------------------------------
|
|
903
|
+
/**
|
|
904
|
+
* Replace a section's ENTIRE body with `newBody`, discarding whatever was
|
|
905
|
+
* there — the "wholesale reset" write pattern used by milestoneComplete's
|
|
906
|
+
* closure write (## Current Position / ## Operator Next Steps). Retires the
|
|
907
|
+
* fence-blind raw regex `(##\s*<heading>\s*\n)([\s\S]*?)(?=\n##|$)`, which a
|
|
908
|
+
* literal `##` inside a fenced code block in the section body could fool into
|
|
909
|
+
* stopping early (the #2130/#2067/#2080 truncation class) — heading location
|
|
910
|
+
* here goes through `tokenizeHeadings`, which is fence-aware.
|
|
911
|
+
*
|
|
912
|
+
* Byte-parity note: the retired regex's greedy `\s*` (before its mandatory
|
|
913
|
+
* `\n`) swallowed any blank line(s) immediately after the heading into the
|
|
914
|
+
* discarded match, and its non-greedy body match always left exactly ONE
|
|
915
|
+
* newline unconsumed before the next heading (or EOF), regardless of how many
|
|
916
|
+
* blank lines originally separated the section from what followed. Both
|
|
917
|
+
* edges are reproduced explicitly (rather than delegated to `collectSection`'s
|
|
918
|
+
* `trimEnd()`-based body, which trims a *different* amount and would drift
|
|
919
|
+
* the surrounding blank-line count) so `newBody`'s own leading/trailing
|
|
920
|
+
* formatting is exactly what appears in the output.
|
|
921
|
+
*
|
|
922
|
+
* Returns `null` when no heading matches `headingPredicate` (mirrors the
|
|
923
|
+
* retired regex's `pattern.test(body)` miss) — callers fall back to their own
|
|
924
|
+
* append-a-new-section path.
|
|
925
|
+
*/
|
|
926
|
+
function resetSectionVerbatim(content, headingPredicate, newBody) {
|
|
927
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
928
|
+
const idx = headings.findIndex(headingPredicate);
|
|
929
|
+
if (idx === -1)
|
|
930
|
+
return null;
|
|
931
|
+
const target = headings[idx];
|
|
932
|
+
const lines = content.split('\n');
|
|
933
|
+
const headingLineEnd = target.offset + lines[target.line - 1].length + 1;
|
|
934
|
+
// Swallow blank line(s) immediately after the heading (mirrors the retired
|
|
935
|
+
// regex's greedy `\s*` folding them into the discarded match).
|
|
936
|
+
//
|
|
937
|
+
// F7 (#2245 review, nit): recognise a CRLF blank line (`\r\n`), not only a
|
|
938
|
+
// bare LF — a lone `content[bodyStart] === '\n'` check never advances past
|
|
939
|
+
// a `\r` byte, so on a CRLF STATE.md the blank line right after the
|
|
940
|
+
// heading fell into the DISCARDED [bodyStart, bodyEnd) span instead of the
|
|
941
|
+
// KEPT prefix, silently dropping one blank line (contradicting this
|
|
942
|
+
// function's own byte-parity docstring).
|
|
943
|
+
let bodyStart = headingLineEnd;
|
|
944
|
+
while (bodyStart < content.length) {
|
|
945
|
+
if (content[bodyStart] === '\n') {
|
|
946
|
+
bodyStart += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (content[bodyStart] === '\r' && content[bodyStart + 1] === '\n') {
|
|
950
|
+
bodyStart += 2;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
// Stop at the next heading of level >= 2 (mirrors the retired regex's
|
|
956
|
+
// literal `##` lookahead, which matches any ATX heading two-or-more levels
|
|
957
|
+
// deep); leave exactly one newline unconsumed before it, or run to EOF.
|
|
958
|
+
let bodyEnd = content.length;
|
|
959
|
+
for (let j = idx + 1; j < headings.length; j++) {
|
|
960
|
+
if (STOP_H2_PLUS(headings[j].level)) {
|
|
961
|
+
bodyEnd = headings[j].offset - 1;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return content.slice(0, bodyStart) + newBody + content.slice(bodyEnd);
|
|
966
|
+
}
|
|
906
967
|
/**
|
|
907
968
|
* Apply a `milestoneComplete` transition to STATE.md content.
|
|
908
969
|
*
|
|
@@ -917,17 +978,11 @@ function milestoneSwitchCore(content, intent, deps) {
|
|
|
917
978
|
* runtime-specific next-milestone slash command, injecting it via
|
|
918
979
|
* `intent.nextMilestoneCommand` so the core stays pure.
|
|
919
980
|
*
|
|
920
|
-
* The two section resets use raw regex (with the pre-seam `allow-adhoc-markdown`
|
|
921
|
-
* waivers carried from milestone.cts) rather than tokenizeHeadings because the
|
|
922
|
-
* `## Operator Next Steps` section is non-canonical (not in STATE_MD_SECTIONS)
|
|
923
|
-
* and the existing behavior + its tests pin the exact regex semantics. A future
|
|
924
|
-
* collectSection migration (#1372) can swap both to section primitives.
|
|
925
|
-
*
|
|
926
981
|
* Behavior is byte-for-byte with the pre-migration milestone.cts:314-353 block.
|
|
927
982
|
*/
|
|
928
983
|
function milestoneCompleteCore(content, intent, deps) {
|
|
929
984
|
const updated = [];
|
|
930
|
-
const today = deps.clock.
|
|
985
|
+
const today = deps.clock.localToday();
|
|
931
986
|
const version = intent.version;
|
|
932
987
|
for (const fmKey of ['status', 'last_activity', 'last_activity_desc']) {
|
|
933
988
|
const cls = getFieldClassification(fmKey);
|
|
@@ -963,22 +1018,22 @@ function milestoneCompleteCore(content, intent, deps) {
|
|
|
963
1018
|
}
|
|
964
1019
|
// ## Current Position reset — stop resume/progress flows pointing at closed
|
|
965
1020
|
// execution instructions.
|
|
966
|
-
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify carried from milestone.cts; pending collectSection migration #1372
|
|
967
1021
|
const closedPositionBody = `\nPhase: Milestone ${version} complete\n` +
|
|
968
1022
|
`Plan: —\n` +
|
|
969
1023
|
`Status: Awaiting next milestone\n` +
|
|
970
1024
|
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
|
|
971
|
-
|
|
972
|
-
|
|
1025
|
+
const positionReset = resetSectionVerbatim(body, (h) => h.level === 2 && /^current\s+position$/i.test(h.text), closedPositionBody);
|
|
1026
|
+
if (positionReset !== null) {
|
|
1027
|
+
body = positionReset;
|
|
973
1028
|
}
|
|
974
1029
|
else {
|
|
975
1030
|
body = `${body.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
|
|
976
1031
|
}
|
|
977
1032
|
updated.push('Current Position');
|
|
978
1033
|
// ## Operator Next Steps — normalize stale tails that can persist after close.
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
body =
|
|
1034
|
+
const operatorReset = resetSectionVerbatim(body, (h) => h.level === 2 && /^operator\s+next\s+steps$/i.test(h.text), `\n- Start the next milestone with ${intent.nextMilestoneCommand}\n\n`);
|
|
1035
|
+
if (operatorReset !== null) {
|
|
1036
|
+
body = operatorReset;
|
|
982
1037
|
}
|
|
983
1038
|
else {
|
|
984
1039
|
body = `${body.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with ${intent.nextMilestoneCommand}\n`;
|
|
@@ -1175,7 +1230,7 @@ function pruneCore(content, intent) {
|
|
|
1175
1230
|
* (rather than silently writing fallback-derived wrong values).
|
|
1176
1231
|
*/
|
|
1177
1232
|
function syncCore(content, intent, deps) {
|
|
1178
|
-
const today = deps.clock.
|
|
1233
|
+
const today = deps.clock.localToday();
|
|
1179
1234
|
const changes = [];
|
|
1180
1235
|
let modified = content;
|
|
1181
1236
|
const updated = [];
|