diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json index 6a5a2b068..7e28fe76a 100644 --- a/manifests/context-packs/skill-triggers@1.json +++ b/manifests/context-packs/skill-triggers@1.json @@ -10,7 +10,7 @@ "id": "hand-seeded", "source": "manual-curation-pending-regeneration" }, - "registryDigest": "abd597fc032cfab681b4083339491070034f86c947503194499b82771daada8a", + "registryDigest": "6a6a4fd5cc80ecda18184f6b35625c99ccc1bb0036c5e30316fa179ae81e451f", "schemaVersion": 1, "triggers": { "skill:agent-runtime-gateway-smoke-test": [ diff --git a/skills/story-lifecycle/scripts/ledger.js b/skills/story-lifecycle/scripts/ledger.js index 969cf9675..925d4dbe6 100644 --- a/skills/story-lifecycle/scripts/ledger.js +++ b/skills/story-lifecycle/scripts/ledger.js @@ -232,10 +232,15 @@ function allocate(root, dir, epic, count) { fs.mkdirSync(reg, { recursive: true }); const taken = [...fs.readdirSync(dir), ...fs.readdirSync(reg).map((f) => `${f}.md`)] .map((f) => f.match(STORY_FILE)).filter((m) => m && m[1] === epic).map((m) => Number(m[2])); + if (taken.some((n) => !Number.isSafeInteger(n))) { + throw new LedgerError('existing story ID is not a safe integer'); + } let n = Math.max(0, ...taken); const ids = []; while (ids.length < count) { - const id = `${epic}-${++n}`; + n += 1; + if (!Number.isSafeInteger(n)) throw new LedgerError('story ID exceeds safe integer range'); + const id = `${epic}-${n}`; try { fs.writeFileSync(path.join(reg, id), '', { flag: 'wx' }); ids.push(id); diff --git a/tests/docs/story-lifecycle-ledger.test.js b/tests/docs/story-lifecycle-ledger.test.js index 20443a87a..c9d0a7491 100644 --- a/tests/docs/story-lifecycle-ledger.test.js +++ b/tests/docs/story-lifecycle-ledger.test.js @@ -380,6 +380,22 @@ fs.readFileSync = function (p, ...a) { const r = orig.call(fs, p, ...a); } }], + ['allocation rejects unsafe existing numeric IDs instead of looping forever', () => { + const root = newRepo(); + const registry = path.join(root, '.git', 'story-lifecycle', 'ids'); + fs.mkdirSync(registry, { recursive: true }); + for (const n of ['9007199254740992', '9007199254740991']) { + const marker = path.join(registry, `auth-flow-${n}`); + fs.writeFileSync(marker, ''); + const result = spawnSync(process.execPath, [LEDGER, 'allocate', '--root', root, + '--epic', 'auth-flow', '--count', '2'], { encoding: 'utf8', timeout: 2000 }); + assert.strictEqual(result.status, 2, result.stderr || result.error?.message); + assert.match(result.stderr, /safe integer/); + assert.deepStrictEqual(fs.readdirSync(registry), [`auth-flow-${n}`]); + fs.rmSync(marker); + } + }], + ['ledger paths are confined to .stories/ file shapes', () => { const dir = path.join(os.tmpdir(), '.stories'); for (const bad of ['../x-1.md', 'epics/../../x.md', 'Auth-1.md', 'a/b-1.md', 'epics/a.txt', 'sprints/sprint-0.md', '/abs-1.md']) {