@opengsd/gsd-core 1.20.0 → 1.20.1
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/bin/install.js +36 -40
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +104 -70
- package/gsd-core/bin/lib/capability-registry.cjs +64 -64
- package/gsd-core/bin/lib/commands.cjs +19 -0
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +7 -7
- package/gsd-core/bin/lib/install-engine.cjs +9 -9
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase.cjs +64 -15
- package/gsd-core/bin/lib/planning-snapshot.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +29 -0
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -130
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/state-transition.cjs +12 -5
- package/gsd-core/bin/lib/state.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/hooks/dist/gsd-agent-isolation-guard.js +1 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
- package/hooks/dist/lib/isolation-sentinel.js +77 -2
- package/hooks/gsd-agent-isolation-guard.js +1 -1
- package/hooks/gsd-cursor-subagent-start.js +1 -1
- package/hooks/lib/isolation-sentinel.js +77 -2
- package/package.json +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/lib/macos-conformance-tier.generated.cjs +4 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +4 -0
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -14,6 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
17
|
+
const install_manifest_record_cjs_1 = require("./install-manifest-record.cjs");
|
|
17
18
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
18
19
|
const security_cjs_1 = require("./security.cjs");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -835,6 +836,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
835
836
|
// cmdEffortSyncCodex and cmdEffortSyncOpencode, so a fault mid-write can
|
|
836
837
|
// never leave an agent file truncated or empty.
|
|
837
838
|
let fileFailureCount = 0;
|
|
839
|
+
const rewrittenFiles = [];
|
|
838
840
|
for (const file of files) {
|
|
839
841
|
const agentName = file.replace(/\.md$/, '');
|
|
840
842
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
@@ -852,6 +854,8 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
852
854
|
fileFailureCount++;
|
|
853
855
|
continue;
|
|
854
856
|
}
|
|
857
|
+
// #5683: hash BEFORE any rewrite — only a GSD-owned file gets re-recorded.
|
|
858
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath);
|
|
855
859
|
// Resolve using install-time logic: home defaults merged with project config.
|
|
856
860
|
const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
|
|
857
861
|
// #3533 (10d): 'inherit' means the key must NOT exist. An absent key is
|
|
@@ -915,6 +919,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
915
919
|
}
|
|
916
920
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
917
921
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathInherit, filePath);
|
|
922
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
918
923
|
}
|
|
919
924
|
catch {
|
|
920
925
|
try {
|
|
@@ -992,6 +997,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
992
997
|
}
|
|
993
998
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
994
999
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathSet, filePath);
|
|
1000
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
995
1001
|
}
|
|
996
1002
|
catch {
|
|
997
1003
|
try {
|
|
@@ -1006,6 +1012,9 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
1006
1012
|
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
1007
1013
|
synced++;
|
|
1008
1014
|
}
|
|
1015
|
+
// #5683: keep the install manifest in step with the agents just rewritten.
|
|
1016
|
+
if (!dryRun)
|
|
1017
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1009
1018
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, fileFailureCount > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1010
1019
|
}
|
|
1011
1020
|
/**
|
|
@@ -1048,6 +1057,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1048
1057
|
const refused = [];
|
|
1049
1058
|
const writeFailures = [];
|
|
1050
1059
|
const readFailures = [];
|
|
1060
|
+
const rewrittenFiles = [];
|
|
1051
1061
|
let synced = 0;
|
|
1052
1062
|
let skipped = 0;
|
|
1053
1063
|
for (const file of files) {
|
|
@@ -1066,6 +1076,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1066
1076
|
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1067
1077
|
continue;
|
|
1068
1078
|
}
|
|
1079
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath); // #5683
|
|
1069
1080
|
const parsed = (0, codex_agent_toml_cjs_1.parseCodexAgentToml)(content);
|
|
1070
1081
|
if (!parsed.ok) {
|
|
1071
1082
|
// Never partially rewritten (40-design.md, ADR-2313 reader/writer
|
|
@@ -1134,6 +1145,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1134
1145
|
}
|
|
1135
1146
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1136
1147
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1148
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
1137
1149
|
}
|
|
1138
1150
|
catch (err) {
|
|
1139
1151
|
// Reported, not thrown — the remaining agents still get processed.
|
|
@@ -1150,6 +1162,8 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1150
1162
|
changes.push(...pendingChanges);
|
|
1151
1163
|
synced++;
|
|
1152
1164
|
}
|
|
1165
|
+
if (!dryRun)
|
|
1166
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1153
1167
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, refused, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1154
1168
|
}
|
|
1155
1169
|
/**
|
|
@@ -1198,6 +1212,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1198
1212
|
const changes = [];
|
|
1199
1213
|
const writeFailures = [];
|
|
1200
1214
|
const readFailures = [];
|
|
1215
|
+
const rewrittenFiles = [];
|
|
1201
1216
|
let synced = 0;
|
|
1202
1217
|
let skipped = 0;
|
|
1203
1218
|
for (const file of files) {
|
|
@@ -1216,6 +1231,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1216
1231
|
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1217
1232
|
continue;
|
|
1218
1233
|
}
|
|
1234
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath); // #5683
|
|
1219
1235
|
// `target === null` covers both "no effort configured" (inherit) and "a
|
|
1220
1236
|
// level OpenCode does not accept" — both must produce NO `variant:` key,
|
|
1221
1237
|
// exactly what install writes.
|
|
@@ -1312,6 +1328,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1312
1328
|
}
|
|
1313
1329
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1314
1330
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1331
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
1315
1332
|
}
|
|
1316
1333
|
catch (err) {
|
|
1317
1334
|
try {
|
|
@@ -1340,6 +1357,8 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1340
1357
|
// Making the exit code reflect failure would be a CLI-contract change
|
|
1341
1358
|
// affecting all three cmdEffortSync* branches (claude, codex, opencode) and
|
|
1342
1359
|
// is out of scope for this fix.
|
|
1360
|
+
if (!dryRun)
|
|
1361
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1343
1362
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1344
1363
|
}
|
|
1345
1364
|
/**
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool-owned `.gsd/` directory owner (#5757).
|
|
4
|
+
*
|
|
5
|
+
* gsd-core keeps small tool state under `<project>/.gsd/` (the dispatch
|
|
6
|
+
* isolation sentinel, the harness fork-probe cache). A user project does not
|
|
7
|
+
* inherit gsd-core's own root `.gitignore`, and the project may contain a
|
|
8
|
+
* `.gsd` symlink, so every writer needs the same two guarantees before it
|
|
9
|
+
* writes anything there:
|
|
10
|
+
*
|
|
11
|
+
* 1. the directory is a real directory that resolves inside the project
|
|
12
|
+
* (never write through a project-supplied symlink or ancestor redirect);
|
|
13
|
+
* 2. the directory is ignored by git (`.gsd/.gitignore`), without clobbering
|
|
14
|
+
* rules a user already put there.
|
|
15
|
+
*
|
|
16
|
+
* This module is the single owner of both. Callers pass their own ignore-file
|
|
17
|
+
* `marker` comment and the `rules` they need; the owner appends only the rules
|
|
18
|
+
* that are missing.
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.GSD_IGNORE_SYMLINK = exports.GSD_DIR_UNSAFE = void 0;
|
|
25
|
+
exports.ignoreAdditions = ignoreAdditions;
|
|
26
|
+
exports.ensureToolOwnedGsdDir = ensureToolOwnedGsdDir;
|
|
27
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
28
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
31
|
+
const SYMLINK_IGNORE_MESSAGE = '.gsd/.gitignore is a symlink';
|
|
32
|
+
/** Stable `code` values on the refusal errors; callers translate by code, never by message. */
|
|
33
|
+
exports.GSD_DIR_UNSAFE = 'GSD_DIR_UNSAFE';
|
|
34
|
+
exports.GSD_IGNORE_SYMLINK = 'GSD_IGNORE_SYMLINK';
|
|
35
|
+
function codedError(message, code) {
|
|
36
|
+
return Object.assign(new Error(message), { code });
|
|
37
|
+
}
|
|
38
|
+
function hasLine(text, line) {
|
|
39
|
+
return text.split('\n').some((l) => l.replace(/\r$/, '') === line);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pure merge: the text to append to an existing ignore file so that `marker`
|
|
43
|
+
* and every rule are present, or '' when nothing is missing. A file that
|
|
44
|
+
* already ignores everything (`*` line) needs nothing.
|
|
45
|
+
*/
|
|
46
|
+
function ignoreAdditions(current, { marker, rules }) {
|
|
47
|
+
if (hasLine(current, '*'))
|
|
48
|
+
return '';
|
|
49
|
+
const missing = rules.filter((rule) => !hasLine(current, rule));
|
|
50
|
+
if (missing.length === 0)
|
|
51
|
+
return '';
|
|
52
|
+
const lead = current === '' || current.endsWith('\n') ? '' : '\n';
|
|
53
|
+
const markerLine = current.includes(marker) ? '' : `${marker}\n`;
|
|
54
|
+
return `${lead}${markerLine}${missing.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
function isLinkError(err) {
|
|
57
|
+
const e = err;
|
|
58
|
+
return !!e && (e.code === 'ELOOP' || e.message === SYMLINK_IGNORE_MESSAGE);
|
|
59
|
+
}
|
|
60
|
+
function mergeExistingIgnore(ignorePath, options) {
|
|
61
|
+
let fd;
|
|
62
|
+
let tmpPath = null;
|
|
63
|
+
try {
|
|
64
|
+
if (node_fs_1.default.lstatSync(ignorePath).isSymbolicLink())
|
|
65
|
+
throw new Error(SYMLINK_IGNORE_MESSAGE);
|
|
66
|
+
// Read through an fd opened without following links where supported
|
|
67
|
+
// (O_NOFOLLOW is absent on Windows, hence `|| 0`), then replace by rename
|
|
68
|
+
// rather than append through the path, so a link swapped in after the
|
|
69
|
+
// check cannot redirect a write elsewhere.
|
|
70
|
+
fd = node_fs_1.default.openSync(ignorePath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW || 0));
|
|
71
|
+
if (!node_fs_1.default.fstatSync(fd).isFile())
|
|
72
|
+
throw new Error('.gsd/.gitignore is not a regular file');
|
|
73
|
+
const current = node_fs_1.default.readFileSync(fd, 'utf8');
|
|
74
|
+
node_fs_1.default.closeSync(fd);
|
|
75
|
+
fd = undefined;
|
|
76
|
+
const additions = ignoreAdditions(current, options);
|
|
77
|
+
if (additions === '')
|
|
78
|
+
return;
|
|
79
|
+
// `tmpPath` is assigned only once the exclusive create succeeded, so an
|
|
80
|
+
// EEXIST never unlinks an entry this process did not create.
|
|
81
|
+
const candidateTmp = `${ignorePath}.tmp-${process.pid}-${node_crypto_1.default.randomBytes(6).toString('hex')}`;
|
|
82
|
+
const tmpFd = node_fs_1.default.openSync(candidateTmp, 'wx');
|
|
83
|
+
tmpPath = candidateTmp;
|
|
84
|
+
try {
|
|
85
|
+
node_fs_1.default.writeFileSync(tmpFd, current + additions);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
node_fs_1.default.closeSync(tmpFd);
|
|
89
|
+
}
|
|
90
|
+
if (node_fs_1.default.lstatSync(ignorePath).isSymbolicLink())
|
|
91
|
+
throw new Error(SYMLINK_IGNORE_MESSAGE);
|
|
92
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, ignorePath);
|
|
93
|
+
tmpPath = null;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (isLinkError(err))
|
|
97
|
+
throw codedError(SYMLINK_IGNORE_MESSAGE, exports.GSD_IGNORE_SYMLINK);
|
|
98
|
+
// Ignore seeding is best-effort; the caller's state is still useful if
|
|
99
|
+
// this path cannot be read or updated.
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (fd !== undefined)
|
|
103
|
+
node_fs_1.default.closeSync(fd);
|
|
104
|
+
if (tmpPath) {
|
|
105
|
+
try {
|
|
106
|
+
node_fs_1.default.unlinkSync(tmpPath);
|
|
107
|
+
}
|
|
108
|
+
catch { /* best-effort cleanup */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Ensure `<cwd>/.gsd` exists as a real, project-contained, git-ignored
|
|
114
|
+
* directory and return its path. Throws when `.gsd` is a symlink, resolves
|
|
115
|
+
* outside the project, or `.gsd/.gitignore` is a symlink.
|
|
116
|
+
*/
|
|
117
|
+
function ensureToolOwnedGsdDir(cwd, options) {
|
|
118
|
+
const dir = node_path_1.default.join(cwd, '.gsd');
|
|
119
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
120
|
+
if (node_fs_1.default.lstatSync(dir).isSymbolicLink() ||
|
|
121
|
+
node_path_1.default.relative(node_fs_1.default.realpathSync(cwd), node_fs_1.default.realpathSync(dir)) !== '.gsd') {
|
|
122
|
+
throw codedError('.gsd directory is a symlink or resolves outside the project', exports.GSD_DIR_UNSAFE);
|
|
123
|
+
}
|
|
124
|
+
const ignorePath = node_path_1.default.join(dir, '.gitignore');
|
|
125
|
+
try {
|
|
126
|
+
node_fs_1.default.writeFileSync(ignorePath, '*\n', { flag: 'wx' });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err.code === 'EEXIST')
|
|
130
|
+
mergeExistingIgnore(ignorePath, options);
|
|
131
|
+
// Any other ignore-file IO failure must not prevent the caller's write.
|
|
132
|
+
}
|
|
133
|
+
return dir;
|
|
134
|
+
}
|
|
@@ -99,7 +99,7 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
|
|
|
99
99
|
const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs, } = phaseLocator;
|
|
100
100
|
const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
101
101
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
102
|
-
const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
102
|
+
const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseNumberTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
103
103
|
const { phaseToken } = phaseIdDisplayMod;
|
|
104
104
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
105
105
|
const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, findPhaseSpecIn, resolvePhaseIdConvention, worktreesOptedOut, } = planningWorkspace;
|
|
@@ -2626,7 +2626,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2626
2626
|
}
|
|
2627
2627
|
}
|
|
2628
2628
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2629
|
-
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2629
|
+
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext, phaseIdConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2630
2630
|
const phases = [];
|
|
2631
2631
|
const bracketIdsByPhaseNumber = new Map();
|
|
2632
2632
|
let match;
|
|
@@ -2650,7 +2650,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2650
2650
|
}
|
|
2651
2651
|
const sectionStart = match.index;
|
|
2652
2652
|
const restOfContent = content.slice(sectionStart);
|
|
2653
|
-
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext)}`, 'i'));
|
|
2653
|
+
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext, phaseIdConvention)}`, 'i'));
|
|
2654
2654
|
const sectionEnd = nextHeader
|
|
2655
2655
|
? sectionStart + nextHeader.index
|
|
2656
2656
|
: content.length;
|
|
@@ -2849,7 +2849,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2849
2849
|
}
|
|
2850
2850
|
else {
|
|
2851
2851
|
// Preserve the historical greedy non-bracket fallback byte-for-byte.
|
|
2852
|
-
const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${
|
|
2852
|
+
const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${phaseNumberTokenSource(phaseIdConvention)})[:\\s]`, 'gi');
|
|
2853
2853
|
let _allMatch;
|
|
2854
2854
|
while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
|
|
2855
2855
|
if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(_allMatch[0]))
|
|
@@ -3433,9 +3433,9 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3433
3433
|
const roadmapPhaseNums = new Set();
|
|
3434
3434
|
const roadmapPhaseNames = new Map();
|
|
3435
3435
|
const roadmapCheckboxStates = new Map();
|
|
3436
|
+
const progressConvention = resolvePhaseIdConvention(cwd);
|
|
3436
3437
|
try {
|
|
3437
3438
|
const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
|
3438
|
-
const progressConvention = resolvePhaseIdConvention(cwd);
|
|
3439
3439
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
|
|
3440
3440
|
// buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
|
|
3441
3441
|
// bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
|
|
@@ -3472,7 +3472,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3472
3472
|
// matched (by absorbing the bracket text). This does NOT migrate the site
|
|
3473
3473
|
// onto phaseHeadingPrefixSrcFor / the bracket convention — that stays
|
|
3474
3474
|
// out of scope per the comment above (#4984 revert, pinned census).
|
|
3475
|
-
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext)})[:\\s]`, 'gim', { state: 'captured' });
|
|
3475
|
+
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext, progressConvention)})[:\\s]`, 'gim', { state: 'captured' });
|
|
3476
3476
|
let cbm;
|
|
3477
3477
|
while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
|
|
3478
3478
|
const cbKey = cbm[2];
|
|
@@ -3494,7 +3494,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3494
3494
|
// behavior-preserving).
|
|
3495
3495
|
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
3496
3496
|
for (const dir of dirs) {
|
|
3497
|
-
const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext)})-?(.*)`, 'i'));
|
|
3497
|
+
const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext, progressConvention)})-?(.*)`, 'i'));
|
|
3498
3498
|
const query = customPhaseQuery(dir, directoryContext);
|
|
3499
3499
|
const owner = query && customDirectoryOwner(dir, query);
|
|
3500
3500
|
const phaseNumber = owner ? owner.id : dirMatch ? dirMatch[1] : dir;
|
|
@@ -229,11 +229,8 @@ const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md'];
|
|
|
229
229
|
* with the computed pathPrefix for the install.
|
|
230
230
|
*/
|
|
231
231
|
function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) {
|
|
232
|
-
|
|
233
|
-
content =
|
|
234
|
-
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
235
|
-
content = content.replace(/~\/\.opencode\//g, pathPrefix);
|
|
236
|
-
content = content.replace(/~\/\.kilo\//g, pathPrefix);
|
|
232
|
+
// #5715: one pass with literal replacement (see rewriteOpencodeFamilyConfigDirForms).
|
|
233
|
+
content = runtimeArtifactConversion._rewriteOpencodeFamilyConfigDirForms(content, pathPrefix, getDirName(runtime));
|
|
237
234
|
// #5637: the typed-text capture heredoc ends at a terminator the typed text cannot know.
|
|
238
235
|
return (0, args_terminator_cjs_1.stampInstalledContent)(content);
|
|
239
236
|
}
|
|
@@ -1653,7 +1650,8 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1653
1650
|
let content = installFs().readFileSync(node_path_1.default.join(rawDir, entry.name), 'utf8');
|
|
1654
1651
|
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
1655
1652
|
content = processAttribution(content, resolveAttribution(runtime));
|
|
1656
|
-
|
|
1653
|
+
// #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
|
|
1654
|
+
content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, (c) => converter(c, skillName));
|
|
1657
1655
|
const skillDir = node_path_1.default.join(dest, skillName);
|
|
1658
1656
|
installFs().mkdirSync(skillDir, { recursive: true });
|
|
1659
1657
|
installFs().writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
|
|
@@ -1881,9 +1879,11 @@ function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, res
|
|
|
1881
1879
|
// already satisfies the fold-to-descriptor requirement. Only the SKILLS
|
|
1882
1880
|
// converter site above (installOpencodeFamilySkills) has a real
|
|
1883
1881
|
// `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1882
|
+
// #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
|
|
1883
|
+
const frontmatterConverter = hostBehaviorsFor(runtime).frontmatterDialect === 'kilo'
|
|
1884
|
+
? (c) => runtimeArtifactConversion.convertClaudeToKiloFrontmatter(c)
|
|
1885
|
+
: (c) => runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(c);
|
|
1886
|
+
content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, frontmatterConverter);
|
|
1887
1887
|
installFs().writeFileSync(node_path_1.default.join(destDir, destName), content);
|
|
1888
1888
|
}
|
|
1889
1889
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Install manifest per-file hash ownership (#5683).
|
|
4
|
+
*
|
|
5
|
+
* `gsd-file-manifest.json` records `sha256(file bytes)` per installed file,
|
|
6
|
+
* keyed by path relative to the runtime config dir. `effort sync --apply`
|
|
7
|
+
* rewrites installed agents, so it re-records exactly those files here.
|
|
8
|
+
* `bin/install.js` fileHash delegates to this module: one hash function.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.fileHash = fileHash;
|
|
12
|
+
exports.tryFileHash = tryFileHash;
|
|
13
|
+
exports.recordManifestFileHashes = recordManifestFileHashes;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
|
+
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
|
19
|
+
/** SHA256 (lowercase hex) of a file's bytes — the manifest's per-file hash. */
|
|
20
|
+
function fileHash(filePath) {
|
|
21
|
+
return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(filePath)).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/** fileHash that returns null instead of throwing — for capturing a pre-rewrite hash. */
|
|
24
|
+
function tryFileHash(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
return fileHash(filePath);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Update ONLY the manifest entries for `rewritten` files (under `configDir`),
|
|
34
|
+
* and only those that were GSD-owned before the rewrite: `preHash` equal to the
|
|
35
|
+
* recorded manifest entry. A file the user had already edited keeps its stale
|
|
36
|
+
* entry so the next install still detects it and backs it up.
|
|
37
|
+
* Skips silently (returns false) when the manifest is absent, unparseable or
|
|
38
|
+
* not the documented shape; publishes via tmp + rename.
|
|
39
|
+
*/
|
|
40
|
+
function recordManifestFileHashes(configDir, rewritten) {
|
|
41
|
+
if (rewritten.length === 0)
|
|
42
|
+
return false;
|
|
43
|
+
const manifestPath = (0, node_path_1.join)(configDir, MANIFEST_NAME);
|
|
44
|
+
let manifest;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
47
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
48
|
+
return false;
|
|
49
|
+
manifest = parsed;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const files = manifest['files'];
|
|
55
|
+
if (!files || typeof files !== 'object' || Array.isArray(files))
|
|
56
|
+
return false;
|
|
57
|
+
const entries = files;
|
|
58
|
+
let changed = false;
|
|
59
|
+
for (const { path: absPath, preHash } of rewritten) {
|
|
60
|
+
const rel = (0, node_path_1.relative)(configDir, absPath).replace(/\\/g, '/');
|
|
61
|
+
if (rel === '' || rel.startsWith('..') || (0, node_path_1.isAbsolute)(rel))
|
|
62
|
+
continue;
|
|
63
|
+
if (preHash === null || entries[rel] !== preHash)
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
entries[rel] = fileHash(absPath);
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
catch { /* unreadable file: leave its entry as recorded */ }
|
|
70
|
+
}
|
|
71
|
+
if (!changed)
|
|
72
|
+
return false;
|
|
73
|
+
const tmpPath = `${manifestPath}.tmp.${process.pid}`;
|
|
74
|
+
try {
|
|
75
|
+
(0, node_fs_1.writeFileSync)(tmpPath, JSON.stringify(manifest, null, 2));
|
|
76
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, manifestPath);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
try {
|
|
81
|
+
(0, node_fs_1.unlinkSync)(tmpPath);
|
|
82
|
+
}
|
|
83
|
+
catch { /* already gone or never created */ }
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -35,7 +35,7 @@ const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs
|
|
|
35
35
|
const capabilityLedger = require("./capability-ledger.cjs");
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
37
|
const conversionModule = require("./runtime-artifact-conversion.cjs");
|
|
38
|
-
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, } = conversionModule;
|
|
38
|
+
const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, deriveAgentName: _deriveAgentName, applyAgentFrontmatterExtensions: _applyAgentFrontmatterExtensions, appendAgentTools: _appendAgentTools, _shieldAgentConverterPrefix, } = conversionModule;
|
|
39
39
|
// #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
|
|
40
40
|
// stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
|
|
41
41
|
// `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
|
|
@@ -1011,7 +1011,9 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
1011
1011
|
// Step 3: validated canonical grants, before host conversion.
|
|
1012
1012
|
content = _appendAgentTools(content, grants);
|
|
1013
1013
|
// Step 4: converter (runtime-specific frontmatter/body transform)
|
|
1014
|
-
|
|
1014
|
+
// #5715: the converter runs after the prefix pass above; shield the emitted prefix
|
|
1015
|
+
// from it, except where the runtime declares no path rewrite (nothing was emitted).
|
|
1016
|
+
content = _shieldAgentConverterPrefix(content, agentCtx.runtime, agentCtx.pathPrefix, (shielded) => converter(shielded, isGlobal, { agentName, pathPrefix: agentCtx.pathPrefix }));
|
|
1015
1017
|
// Step 5: frontmatter extensions (effort/disallowedTools; no-op unless
|
|
1016
1018
|
// the runtime declares hostBehaviors.agentFrontmatterExtensions)
|
|
1017
1019
|
content = _applyAgentFrontmatterExtensions(content, { runtime: agentCtx.runtime, agentName, targetDir: agentCtx.targetDir });
|
|
@@ -71,6 +71,8 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
|
|
|
71
71
|
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
|
72
72
|
// introduced outside this module without a `// phase-id-owner:` justification.
|
|
73
73
|
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
|
74
|
+
// #5676: milestone-prefixed M-NN token grammar (one optional -NN segment, the #5626 grammar).
|
|
75
|
+
const PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED = '\\d+(?:-\\d+)?[A-Z]?(?:\\.\\d+)*';
|
|
74
76
|
// #4764: a legacy phase REFERENCE in depends-on PROSE — the token in context, directly
|
|
75
77
|
// following "Phase"/"Phases", with bare-token list continuation ("Phases 1 and 2",
|
|
76
78
|
// "Phase 1, 2, and 3", "Phase 1-3"). Built HERE, beside the token grammar it is
|
|
@@ -381,7 +383,7 @@ function phaseHeadingPrefixSrcFor(baseline, convention, capturing = false) {
|
|
|
381
383
|
function parsePhaseChecklistLine(line, convention, context) {
|
|
382
384
|
const capturesBracketId = convention === 'bracket';
|
|
383
385
|
const intro = phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, capturesBracketId);
|
|
384
|
-
const pattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${intro}(${context ? phaseScanTokenSource(context) :
|
|
386
|
+
const pattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${intro}(${context ? phaseScanTokenSource(context, convention) : phaseNumberTokenSource(convention)})`
|
|
385
387
|
+ `${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])`, 'i', { state: 'captured' });
|
|
386
388
|
const match = pattern.exec(line);
|
|
387
389
|
if (!match)
|
|
@@ -413,7 +415,7 @@ function buildPhaseHeadingScanRegex(baseline, convention, context) {
|
|
|
413
415
|
const bracketGroup = convention === 'bracket' ? 1 : null;
|
|
414
416
|
const phaseNumGroup = bracketGroup ? 2 : 1;
|
|
415
417
|
const phaseNameGroup = phaseNumGroup + 1;
|
|
416
|
-
const regex = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(baseline, convention, true)}(${context && convention !== 'bracket' ? phaseScanTokenSource(context) :
|
|
418
|
+
const regex = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(baseline, convention, true)}(${context && convention !== 'bracket' ? phaseScanTokenSource(context, convention) : phaseNumberTokenSource(convention)})${OPTIONAL_PHASE_TAG_SOURCE}:\\s*([^\\n]+)`, 'gim');
|
|
417
419
|
return { regex, bracketGroup, phaseNumGroup, phaseNameGroup };
|
|
418
420
|
}
|
|
419
421
|
/**
|
|
@@ -435,7 +437,7 @@ function buildPhaseDeclarationScans(convention, withBullets = true) {
|
|
|
435
437
|
// Under bracket a numbered-list item (`1. [ ] **[GSD.02] 05: n**`) is a checklist row too.
|
|
436
438
|
const bullet = bracket ? '^[ \\t]*(?:-|\\d+[.)])[ \\t]*' : '^[ \\t]*-[ \\t]*';
|
|
437
439
|
// Bracket ids may carry a letter suffix (`12A`), as the heading scan's phase token does.
|
|
438
|
-
const tail = bracket ? '(\\d+)(?=[A-Za-z]?(?:[:.\\s*]|$))' : '(\\d+)(?=[:.\\s*]|$)';
|
|
440
|
+
const tail = bracket ? '(\\d+)(?=[A-Za-z]?(?:[:.\\s*]|$))' : convention === 'milestone-prefixed' ? `(${phaseNumberTokenSource(convention)})(?=[:.\\s*]|$)` : '(\\d+)(?=[:.\\s*]|$)';
|
|
439
441
|
const checkbox = `\\[${bracket ? `(?!${BRACKET_ID_SRC}\\])` : ''}[^\\]]{0,200}\\][ \\t]*`;
|
|
440
442
|
if (!bracket) {
|
|
441
443
|
if (withBullets)
|
|
@@ -1582,9 +1584,17 @@ const CUSTOM_PHASE_ID_FILE = '.phase-id';
|
|
|
1582
1584
|
function phaseDirectoryContext(config, roadmap = '', knownIds = []) {
|
|
1583
1585
|
return { phaseNaming: typeof config.phase_naming === 'string' ? config.phase_naming.toLowerCase() : config.phase_naming, projectCode: config.project_code ?? null, roadmap, knownIds };
|
|
1584
1586
|
}
|
|
1585
|
-
/**
|
|
1586
|
-
function
|
|
1587
|
-
return
|
|
1587
|
+
/** #5676: the single owner of the numeric phase-token source per convention. */
|
|
1588
|
+
function phaseNumberTokenSource(convention) {
|
|
1589
|
+
return convention === 'milestone-prefixed' ? PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED : PHASE_NUMBER_TOKEN_SOURCE;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Custom scans reuse the numeric source verbatim outside configured custom projects.
|
|
1593
|
+
* #5676: under `milestone-prefixed` the numeric source admits the `M-NN` token
|
|
1594
|
+
* (`3-02`), so a `03-02-beta` directory is read as phase `3-02`, not phase `3`.
|
|
1595
|
+
*/
|
|
1596
|
+
function phaseScanTokenSource(context, convention) {
|
|
1597
|
+
return context?.phaseNaming === 'custom' ? '[^ \t:\n]+' : phaseNumberTokenSource(convention);
|
|
1588
1598
|
}
|
|
1589
1599
|
const customIdentityIndexes = new WeakMap();
|
|
1590
1600
|
function customIdentityIndex(options) {
|
|
@@ -1986,12 +1996,17 @@ function phaseKeyFromDir(dirName, convention, context) {
|
|
|
1986
1996
|
* `Phase:` value. Markdown emphasis is stripped first so a bolded cell is not
|
|
1987
1997
|
* mistaken for a non-phase. Returns null when the value does not BEGIN with a
|
|
1988
1998
|
* phase token (`parsePhaseFromProse` anchoring, #2111).
|
|
1999
|
+
*
|
|
2000
|
+
* #5675: `convention` is the project's `phase_id_convention`. Under `milestone-prefixed` the id is
|
|
2001
|
+
* the whole `M-NN` (`3-01`), so the key equals `phaseKeyFromDir` for the same phase; every other
|
|
2002
|
+
* convention (and an absent one) keeps the leading-integer read. It is the trailing parameter so the
|
|
2003
|
+
* existing `(value, context)` callers stay byte-identical.
|
|
1989
2004
|
*/
|
|
1990
|
-
function phaseKeyFromProse(value, context) {
|
|
2005
|
+
function phaseKeyFromProse(value, context, convention) {
|
|
1991
2006
|
if (value == null)
|
|
1992
2007
|
return null;
|
|
1993
2008
|
const clean = String(value).replace(/[*_`~]/g, '');
|
|
1994
|
-
const { phase } = parsePhaseFromProse(clean,
|
|
2009
|
+
const { phase } = parsePhaseFromProse(clean, convention, context);
|
|
1995
2010
|
return phase === null ? null : phaseKeyFromToken(phase, context);
|
|
1996
2011
|
}
|
|
1997
2012
|
/**
|
|
@@ -2031,7 +2046,7 @@ function isPlannedPhaseStatusPhrase(value) {
|
|
|
2031
2046
|
// (`3-01`), so the number capture takes one optional `-NN` segment; every other convention (and an absent
|
|
2032
2047
|
// one) keeps the dotted-only capture, where `3-01` reads as phase `3` exactly as before.
|
|
2033
2048
|
const PHASE_PROSE_RE = /^\s*(?:Phase\s+)?(?:[A-Z][A-Z0-9_]*-)?(\d+[A-Z]?(?:\.\d+)*)\b/i;
|
|
2034
|
-
const PHASE_PROSE_MILESTONE_PREFIXED_RE =
|
|
2049
|
+
const PHASE_PROSE_MILESTONE_PREFIXED_RE = new RegExp(`^\\s*(?:Phase\\s+)?(?:[A-Z][A-Z0-9_]*-)?(${PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED})\\b`, 'i');
|
|
2035
2050
|
function parsePhaseFromProse(value, convention, context) {
|
|
2036
2051
|
if (!value)
|
|
2037
2052
|
return { phase: null, name: null };
|
|
@@ -2206,6 +2221,8 @@ const phaseIdExports = {
|
|
|
2206
2221
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
2207
2222
|
OPTIONAL_PHASE_TAG_SOURCE,
|
|
2208
2223
|
PHASE_NUMBER_TOKEN_SOURCE,
|
|
2224
|
+
PHASE_NUMBER_TOKEN_SOURCE_MILESTONE_PREFIXED,
|
|
2225
|
+
phaseNumberTokenSource,
|
|
2209
2226
|
PHASE_DEP_REF_SOURCE,
|
|
2210
2227
|
tokenizePhaseDependencyReferences,
|
|
2211
2228
|
extractPhaseDependencyTokens,
|