@izkac/forgekit 0.3.12 → 0.3.13

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.
Files changed (116) hide show
  1. package/bin/forge.mjs +107 -107
  2. package/bin/forgekit.mjs +83 -83
  3. package/bin/review.mjs +81 -81
  4. package/package.json +1 -1
  5. package/scripts/prepack.mjs +78 -78
  6. package/scripts/run-tests.mjs +50 -50
  7. package/src/adr.mjs +236 -236
  8. package/src/adr.test.mjs +170 -170
  9. package/src/change.mjs +327 -234
  10. package/src/change.test.mjs +145 -83
  11. package/src/cleanup-sessions.mjs +84 -84
  12. package/src/config.mjs +103 -103
  13. package/src/defer.mjs +75 -75
  14. package/src/doctor.mjs +350 -341
  15. package/src/doctor.test.mjs +114 -114
  16. package/src/init.mjs +680 -621
  17. package/src/install.mjs +815 -815
  18. package/src/install.test.mjs +180 -180
  19. package/src/integrity-check.mjs +60 -60
  20. package/src/integrity.mjs +682 -682
  21. package/src/integrity.test.mjs +566 -566
  22. package/src/lib.mjs +143 -143
  23. package/src/models.defaults.json +41 -41
  24. package/src/new-session.mjs +99 -99
  25. package/src/openspec-overlays/README.md +19 -19
  26. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
  27. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
  28. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
  29. package/src/paths.mjs +92 -92
  30. package/src/plan-engine.mjs +321 -278
  31. package/src/plan-engine.test.mjs +447 -283
  32. package/src/preferences.defaults.json +78 -78
  33. package/src/preferences.mjs +438 -438
  34. package/src/preferences.test.mjs +174 -174
  35. package/src/record-evidence.mjs +204 -204
  36. package/src/resolve-model.mjs +312 -312
  37. package/src/resolve-model.test.mjs +194 -194
  38. package/src/review/cli.test.mjs +117 -117
  39. package/src/review/export.mjs +172 -172
  40. package/src/review/export.test.mjs +197 -197
  41. package/src/review/fixtures/valid-review.json +42 -42
  42. package/src/review/lib.mjs +894 -894
  43. package/src/review/lib.test.mjs +266 -266
  44. package/src/review/schema.json +196 -196
  45. package/src/review/signals.test.mjs +62 -62
  46. package/src/score-cli.mjs +68 -68
  47. package/src/score.mjs +568 -568
  48. package/src/score.test.mjs +366 -366
  49. package/src/session-reminder.mjs +207 -207
  50. package/src/session-status.mjs +70 -70
  51. package/src/set-models.mjs +186 -186
  52. package/src/set-phase.mjs +205 -205
  53. package/src/set-prefs.mjs +294 -294
  54. package/src/specs-sync.mjs +234 -0
  55. package/src/specs-sync.test.mjs +114 -0
  56. package/src/spine.mjs +93 -93
  57. package/src/triage-prompt.mjs +175 -175
  58. package/src/triage-prompt.test.mjs +50 -50
  59. package/src/vendor-openspec-overlays.mjs +176 -176
  60. package/src/vendor-openspec-overlays.test.mjs +62 -62
  61. package/vendor/skills/archive-to-adr/SKILL.md +149 -149
  62. package/vendor/skills/forge/SKILL.md +136 -136
  63. package/vendor/skills/forge/docs/forge.md +650 -647
  64. package/vendor/skills/forge/phases/brainstorm.md +23 -23
  65. package/vendor/skills/forge/phases/finish.md +90 -87
  66. package/vendor/skills/forge/phases/implement.md +77 -77
  67. package/vendor/skills/forge/phases/plan-openspec.md +60 -60
  68. package/vendor/skills/forge/phases/plan-specs.md +163 -117
  69. package/vendor/skills/forge/phases/review.md +25 -25
  70. package/vendor/skills/forge/phases/verify.md +124 -124
  71. package/vendor/skills/forge/references/forge-layout.md +85 -85
  72. package/vendor/skills/forge/references/pace.md +115 -115
  73. package/vendor/skills/forge/references/plan-routing.md +52 -51
  74. package/vendor/skills/forge/references/runtime-integrity.md +232 -232
  75. package/vendor/skills/forge/references/substantial-work.md +37 -37
  76. package/vendor/skills/forge/references/test-evidence.md +30 -30
  77. package/vendor/skills/forge/references/test-strategy.md +68 -68
  78. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
  79. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
  80. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
  81. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
  82. package/vendor/skills/thorough-code-review/SKILL.md +290 -290
  83. package/vendor/skills/thorough-code-review/examples.md +133 -133
  84. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
  85. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
  86. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
  87. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
  88. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
  89. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
  90. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
  91. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
  92. package/vendor/templates/adr/README.md +7 -7
  93. package/vendor/templates/adr/decisions.md +141 -141
  94. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
  95. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
  96. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
  97. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
  98. package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
  99. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
  100. package/vendor/templates/project/claude/commands/forge-build.md +17 -17
  101. package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
  102. package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
  103. package/vendor/templates/project/claude/commands/forge-status.md +16 -16
  104. package/vendor/templates/project/claude/commands/forge.md +16 -16
  105. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
  106. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
  107. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
  108. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
  109. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
  110. package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
  111. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
  112. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
  113. package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
  114. package/vendor/templates/project/cursor/commands/forge.md +16 -16
  115. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
  116. package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -3
package/src/lib.mjs CHANGED
@@ -1,143 +1,143 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Shared helpers for Forge session management.
4
- */
5
-
6
- import crypto from 'node:crypto';
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import { registerSession } from './lib/fleet.mjs';
10
-
11
- export const REPO_ROOT = process.cwd();
12
- export const FORGE_DIR = path.join(REPO_ROOT, '.forge');
13
- export const SESSIONS_DIR = path.join(FORGE_DIR, 'sessions');
14
- export const ACTIVE_FILE = path.join(FORGE_DIR, 'active.json');
15
- export const RETENTION_DAYS = 14;
16
-
17
- export function ensureForgeLayout() {
18
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
19
- }
20
-
21
- export function slugify(input) {
22
- return String(input)
23
- .toLowerCase()
24
- .replace(/[^a-z0-9]+/g, '-')
25
- .replace(/^-+|-+$/g, '')
26
- .slice(0, 48) || 'session';
27
- }
28
-
29
- export function utcCompactNow() {
30
- return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
31
- }
32
-
33
- export function randomSuffix(bytes = 3) {
34
- return crypto.randomBytes(bytes).toString('hex');
35
- }
36
-
37
- export function makeSessionId(slug) {
38
- return `${utcCompactNow()}-${slugify(slug)}-${randomSuffix()}`;
39
- }
40
-
41
- export function sessionPath(sessionId) {
42
- return path.join(SESSIONS_DIR, sessionId);
43
- }
44
-
45
- export function readJson(filePath) {
46
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
47
- }
48
-
49
- export function writeJson(filePath, data) {
50
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
52
- }
53
-
54
- export function readActive() {
55
- if (!fs.existsSync(ACTIVE_FILE)) return null;
56
- try {
57
- return readJson(ACTIVE_FILE);
58
- } catch {
59
- return null;
60
- }
61
- }
62
-
63
- export function writeActive(sessionId) {
64
- writeJson(ACTIVE_FILE, {
65
- sessionId,
66
- sessionPath: path.relative(REPO_ROOT, sessionPath(sessionId)).replace(/\\/g, '/'),
67
- updatedAt: new Date().toISOString(),
68
- });
69
- }
70
-
71
- export function clearActive() {
72
- if (fs.existsSync(ACTIVE_FILE)) fs.unlinkSync(ACTIVE_FILE);
73
- }
74
-
75
- export function defaultSession(sessionId, slug) {
76
- const now = new Date().toISOString();
77
- return {
78
- id: sessionId,
79
- slug: slugify(slug),
80
- createdAt: now,
81
- updatedAt: now,
82
- phase: 'triage',
83
- planType: null,
84
- openspecChange: null,
85
- forgeSkipped: false,
86
- cursorChatId: null,
87
- tasksTotal: 0,
88
- tasksComplete: 0,
89
- /** Requested pace: auto | thorough | standard | brisk | lite */
90
- pace: 'auto',
91
- /** Concrete pace after auto resolve or explicit pin */
92
- resolvedPace: null,
93
- paceReason: null,
94
- paceSignal: null,
95
- pacePinned: false,
96
- preferencesOverride: null,
97
- };
98
- }
99
-
100
- export function defaultStatus(session) {
101
- return {
102
- sessionId: session.id,
103
- phase: session.phase,
104
- planType: session.planType,
105
- openspecChange: session.openspecChange,
106
- tasksTotal: session.tasksTotal,
107
- tasksComplete: session.tasksComplete,
108
- pace: session.pace ?? null,
109
- resolvedPace: session.resolvedPace ?? null,
110
- paceReason: session.paceReason ?? null,
111
- updatedAt: session.updatedAt,
112
- };
113
- }
114
-
115
- export function scaffoldSessionDirs(dir) {
116
- for (const sub of ['brainstorm', 'tasks', 'reviews']) {
117
- fs.mkdirSync(path.join(dir, sub), { recursive: true });
118
- }
119
- }
120
-
121
- export function loadSession(sessionId) {
122
- const dir = sessionPath(sessionId);
123
- const file = path.join(dir, 'session.json');
124
- if (!fs.existsSync(file)) {
125
- throw new Error(`Session not found: ${sessionId}`);
126
- }
127
- return { dir, session: readJson(file) };
128
- }
129
-
130
- export function saveSession(dir, session) {
131
- session.updatedAt = new Date().toISOString();
132
- writeJson(path.join(dir, 'session.json'), session);
133
- writeJson(path.join(dir, 'status.json'), defaultStatus(session));
134
- // Mirror into ~/.forgekit/fleet so `forge fleet` sees every project's
135
- // sessions. Project root derived from dir (<root>/.forge/sessions/<id>),
136
- // not REPO_ROOT, so callers with explicit dirs mirror correctly too.
137
- registerSession(path.resolve(dir, '..', '..', '..'), session);
138
- }
139
-
140
- export function sessionAgeDays(session) {
141
- const created = new Date(session.createdAt).getTime();
142
- return (Date.now() - created) / (1000 * 60 * 60 * 24);
143
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared helpers for Forge session management.
4
+ */
5
+
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { registerSession } from './lib/fleet.mjs';
10
+
11
+ export const REPO_ROOT = process.cwd();
12
+ export const FORGE_DIR = path.join(REPO_ROOT, '.forge');
13
+ export const SESSIONS_DIR = path.join(FORGE_DIR, 'sessions');
14
+ export const ACTIVE_FILE = path.join(FORGE_DIR, 'active.json');
15
+ export const RETENTION_DAYS = 14;
16
+
17
+ export function ensureForgeLayout() {
18
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
19
+ }
20
+
21
+ export function slugify(input) {
22
+ return String(input)
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ .slice(0, 48) || 'session';
27
+ }
28
+
29
+ export function utcCompactNow() {
30
+ return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
31
+ }
32
+
33
+ export function randomSuffix(bytes = 3) {
34
+ return crypto.randomBytes(bytes).toString('hex');
35
+ }
36
+
37
+ export function makeSessionId(slug) {
38
+ return `${utcCompactNow()}-${slugify(slug)}-${randomSuffix()}`;
39
+ }
40
+
41
+ export function sessionPath(sessionId) {
42
+ return path.join(SESSIONS_DIR, sessionId);
43
+ }
44
+
45
+ export function readJson(filePath) {
46
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
47
+ }
48
+
49
+ export function writeJson(filePath, data) {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
52
+ }
53
+
54
+ export function readActive() {
55
+ if (!fs.existsSync(ACTIVE_FILE)) return null;
56
+ try {
57
+ return readJson(ACTIVE_FILE);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export function writeActive(sessionId) {
64
+ writeJson(ACTIVE_FILE, {
65
+ sessionId,
66
+ sessionPath: path.relative(REPO_ROOT, sessionPath(sessionId)).replace(/\\/g, '/'),
67
+ updatedAt: new Date().toISOString(),
68
+ });
69
+ }
70
+
71
+ export function clearActive() {
72
+ if (fs.existsSync(ACTIVE_FILE)) fs.unlinkSync(ACTIVE_FILE);
73
+ }
74
+
75
+ export function defaultSession(sessionId, slug) {
76
+ const now = new Date().toISOString();
77
+ return {
78
+ id: sessionId,
79
+ slug: slugify(slug),
80
+ createdAt: now,
81
+ updatedAt: now,
82
+ phase: 'triage',
83
+ planType: null,
84
+ openspecChange: null,
85
+ forgeSkipped: false,
86
+ cursorChatId: null,
87
+ tasksTotal: 0,
88
+ tasksComplete: 0,
89
+ /** Requested pace: auto | thorough | standard | brisk | lite */
90
+ pace: 'auto',
91
+ /** Concrete pace after auto resolve or explicit pin */
92
+ resolvedPace: null,
93
+ paceReason: null,
94
+ paceSignal: null,
95
+ pacePinned: false,
96
+ preferencesOverride: null,
97
+ };
98
+ }
99
+
100
+ export function defaultStatus(session) {
101
+ return {
102
+ sessionId: session.id,
103
+ phase: session.phase,
104
+ planType: session.planType,
105
+ openspecChange: session.openspecChange,
106
+ tasksTotal: session.tasksTotal,
107
+ tasksComplete: session.tasksComplete,
108
+ pace: session.pace ?? null,
109
+ resolvedPace: session.resolvedPace ?? null,
110
+ paceReason: session.paceReason ?? null,
111
+ updatedAt: session.updatedAt,
112
+ };
113
+ }
114
+
115
+ export function scaffoldSessionDirs(dir) {
116
+ for (const sub of ['brainstorm', 'tasks', 'reviews']) {
117
+ fs.mkdirSync(path.join(dir, sub), { recursive: true });
118
+ }
119
+ }
120
+
121
+ export function loadSession(sessionId) {
122
+ const dir = sessionPath(sessionId);
123
+ const file = path.join(dir, 'session.json');
124
+ if (!fs.existsSync(file)) {
125
+ throw new Error(`Session not found: ${sessionId}`);
126
+ }
127
+ return { dir, session: readJson(file) };
128
+ }
129
+
130
+ export function saveSession(dir, session) {
131
+ session.updatedAt = new Date().toISOString();
132
+ writeJson(path.join(dir, 'session.json'), session);
133
+ writeJson(path.join(dir, 'status.json'), defaultStatus(session));
134
+ // Mirror into ~/.forgekit/fleet so `forge fleet` sees every project's
135
+ // sessions. Project root derived from dir (<root>/.forge/sessions/<id>),
136
+ // not REPO_ROOT, so callers with explicit dirs mirror correctly too.
137
+ registerSession(path.resolve(dir, '..', '..', '..'), session);
138
+ }
139
+
140
+ export function sessionAgeDays(session) {
141
+ const created = new Date(session.createdAt).getTime();
142
+ return (Date.now() - created) / (1000 * 60 * 60 * 24);
143
+ }
@@ -1,41 +1,41 @@
1
- {
2
- "billing": "included",
3
- "agents": {
4
- "cursor": {
5
- "included": {
6
- "fast": "inherit",
7
- "standard": "inherit",
8
- "capable": "inherit"
9
- },
10
- "metered": {
11
- "fast": "gpt-5.6-terra-medium",
12
- "standard": "claude-sonnet-5-thinking-high",
13
- "capable": "claude-opus-4-8-thinking-high"
14
- }
15
- },
16
- "claude-code": {
17
- "included": {
18
- "fast": "haiku",
19
- "standard": "sonnet",
20
- "capable": "opus"
21
- },
22
- "metered": {
23
- "fast": "haiku",
24
- "standard": "sonnet",
25
- "capable": "opus"
26
- }
27
- },
28
- "codex": {
29
- "included": {
30
- "fast": "inherit",
31
- "standard": "inherit",
32
- "capable": "inherit"
33
- },
34
- "metered": {
35
- "fast": "inherit",
36
- "standard": "inherit",
37
- "capable": "inherit"
38
- }
39
- }
40
- }
41
- }
1
+ {
2
+ "billing": "included",
3
+ "agents": {
4
+ "cursor": {
5
+ "included": {
6
+ "fast": "inherit",
7
+ "standard": "inherit",
8
+ "capable": "inherit"
9
+ },
10
+ "metered": {
11
+ "fast": "gpt-5.6-terra-medium",
12
+ "standard": "claude-sonnet-5-thinking-high",
13
+ "capable": "claude-opus-4-8-thinking-high"
14
+ }
15
+ },
16
+ "claude-code": {
17
+ "included": {
18
+ "fast": "haiku",
19
+ "standard": "sonnet",
20
+ "capable": "opus"
21
+ },
22
+ "metered": {
23
+ "fast": "haiku",
24
+ "standard": "sonnet",
25
+ "capable": "opus"
26
+ }
27
+ },
28
+ "codex": {
29
+ "included": {
30
+ "fast": "inherit",
31
+ "standard": "inherit",
32
+ "capable": "inherit"
33
+ },
34
+ "metered": {
35
+ "fast": "inherit",
36
+ "standard": "inherit",
37
+ "capable": "inherit"
38
+ }
39
+ }
40
+ }
41
+ }
@@ -1,99 +1,99 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Create a new Forge session under .forge/sessions/ and set it active.
4
- *
5
- * Usage:
6
- * forge new mercury-console-validation
7
- * forge new <slug> [--chat-id <id>] [--signal <text>]
8
- */
9
-
10
- import {
11
- defaultSession,
12
- defaultStatus,
13
- ensureForgeLayout,
14
- FORGE_DIR,
15
- makeSessionId,
16
- saveSession,
17
- scaffoldSessionDirs,
18
- sessionPath,
19
- writeActive,
20
- } from './lib.mjs';
21
- import { resolveSessionPaceFields } from './preferences.mjs';
22
- import { warnIfDoctorFails } from './doctor.mjs';
23
- import { liveOverlaps, queueMessage, sessionDirFor } from './lib/fleet.mjs';
24
-
25
- function usage() {
26
- process.stderr.write(
27
- 'Usage: forge new <slug> [--chat-id <id>] [--signal <text>]\n',
28
- );
29
- process.exit(1);
30
- }
31
-
32
- const args = process.argv.slice(2);
33
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') usage();
34
-
35
- const slug = args[0];
36
- let cursorChatId = null;
37
- let signalText = null;
38
- for (let i = 1; i < args.length; i += 1) {
39
- if (args[i] === '--chat-id' && args[i + 1]) {
40
- cursorChatId = args[i + 1];
41
- i += 1;
42
- } else if (args[i] === '--signal' && args[i + 1]) {
43
- signalText = args[i + 1];
44
- i += 1;
45
- }
46
- }
47
-
48
- ensureForgeLayout();
49
- warnIfDoctorFails({ cwd: process.cwd() });
50
-
51
- const sessionId = makeSessionId(slug);
52
- const dir = sessionPath(sessionId);
53
- scaffoldSessionDirs(dir);
54
-
55
- const session = defaultSession(sessionId, slug);
56
- if (cursorChatId) session.cursorChatId = cursorChatId;
57
-
58
- const paceFields = resolveSessionPaceFields({
59
- forgeDir: FORGE_DIR,
60
- slug: session.slug,
61
- signalText: signalText || session.slug,
62
- });
63
- Object.assign(session, paceFields);
64
-
65
- saveSession(dir, session);
66
- writeActive(sessionId);
67
-
68
- // Fleet coordination: another live session in this working tree risks
69
- // conflicting edits — surface it here and notify the other sessions' inboxes.
70
- const overlaps = liveOverlaps(process.cwd(), sessionId);
71
- for (const o of overlaps) {
72
- queueMessage(
73
- sessionDirFor(o),
74
- `Fleet overlap: session "${session.slug}" (${sessionId}) just started in this project. Coordinate with the user to avoid conflicting edits.`,
75
- );
76
- }
77
-
78
- const out = {
79
- sessionId,
80
- dir,
81
- session: defaultStatus(session),
82
- pace: {
83
- requested: session.pace,
84
- resolved: session.resolvedPace,
85
- reason: session.paceReason,
86
- },
87
- };
88
- if (overlaps.length > 0) {
89
- out.overlaps = overlaps.map((o) => ({
90
- sessionId: o.sessionId,
91
- slug: o.slug,
92
- phase: o.phase,
93
- engine: o.engine,
94
- lastSeen: o.lastSeen ?? o.updatedAt,
95
- }));
96
- out.overlapAdvice =
97
- 'Other live sessions are working in this project. Tell the user and ask: continue anyway, use a git worktree, or pause one session.';
98
- }
99
- process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Create a new Forge session under .forge/sessions/ and set it active.
4
+ *
5
+ * Usage:
6
+ * forge new mercury-console-validation
7
+ * forge new <slug> [--chat-id <id>] [--signal <text>]
8
+ */
9
+
10
+ import {
11
+ defaultSession,
12
+ defaultStatus,
13
+ ensureForgeLayout,
14
+ FORGE_DIR,
15
+ makeSessionId,
16
+ saveSession,
17
+ scaffoldSessionDirs,
18
+ sessionPath,
19
+ writeActive,
20
+ } from './lib.mjs';
21
+ import { resolveSessionPaceFields } from './preferences.mjs';
22
+ import { warnIfDoctorFails } from './doctor.mjs';
23
+ import { liveOverlaps, queueMessage, sessionDirFor } from './lib/fleet.mjs';
24
+
25
+ function usage() {
26
+ process.stderr.write(
27
+ 'Usage: forge new <slug> [--chat-id <id>] [--signal <text>]\n',
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ const args = process.argv.slice(2);
33
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') usage();
34
+
35
+ const slug = args[0];
36
+ let cursorChatId = null;
37
+ let signalText = null;
38
+ for (let i = 1; i < args.length; i += 1) {
39
+ if (args[i] === '--chat-id' && args[i + 1]) {
40
+ cursorChatId = args[i + 1];
41
+ i += 1;
42
+ } else if (args[i] === '--signal' && args[i + 1]) {
43
+ signalText = args[i + 1];
44
+ i += 1;
45
+ }
46
+ }
47
+
48
+ ensureForgeLayout();
49
+ warnIfDoctorFails({ cwd: process.cwd() });
50
+
51
+ const sessionId = makeSessionId(slug);
52
+ const dir = sessionPath(sessionId);
53
+ scaffoldSessionDirs(dir);
54
+
55
+ const session = defaultSession(sessionId, slug);
56
+ if (cursorChatId) session.cursorChatId = cursorChatId;
57
+
58
+ const paceFields = resolveSessionPaceFields({
59
+ forgeDir: FORGE_DIR,
60
+ slug: session.slug,
61
+ signalText: signalText || session.slug,
62
+ });
63
+ Object.assign(session, paceFields);
64
+
65
+ saveSession(dir, session);
66
+ writeActive(sessionId);
67
+
68
+ // Fleet coordination: another live session in this working tree risks
69
+ // conflicting edits — surface it here and notify the other sessions' inboxes.
70
+ const overlaps = liveOverlaps(process.cwd(), sessionId);
71
+ for (const o of overlaps) {
72
+ queueMessage(
73
+ sessionDirFor(o),
74
+ `Fleet overlap: session "${session.slug}" (${sessionId}) just started in this project. Coordinate with the user to avoid conflicting edits.`,
75
+ );
76
+ }
77
+
78
+ const out = {
79
+ sessionId,
80
+ dir,
81
+ session: defaultStatus(session),
82
+ pace: {
83
+ requested: session.pace,
84
+ resolved: session.resolvedPace,
85
+ reason: session.paceReason,
86
+ },
87
+ };
88
+ if (overlaps.length > 0) {
89
+ out.overlaps = overlaps.map((o) => ({
90
+ sessionId: o.sessionId,
91
+ slug: o.slug,
92
+ phase: o.phase,
93
+ engine: o.engine,
94
+ lastSeen: o.lastSeen ?? o.updatedAt,
95
+ }));
96
+ out.overlapAdvice =
97
+ 'Other live sessions are working in this project. Tell the user and ask: continue anyway, use a git worktree, or pause one session.';
98
+ }
99
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
@@ -1,19 +1,19 @@
1
- # OpenSpec Forge overlays
2
-
3
- Vendor OpenSpec skills and `/opsx:*` commands are upgraded in place. **Do not hand-edit** them.
4
-
5
- Forge adaptations live here and are applied by:
6
-
7
- ```bash
8
- forge overlay
9
- ```
10
-
11
- Run after refreshing OpenSpec skills/commands from upstream (usually via `forge init --overlay`).
12
-
13
- | Overlay | Targets |
14
- | ------- | ------- |
15
- | `opsx-apply-implement-step.md` | Step 6 in `opsx-apply` / `opsx/apply` commands |
16
- | `opsx-apply-completion-step.md` | Step 7 “all done” bullet in same commands |
17
- | `openspec-apply-change-footer.md` | Footer on `openspec-apply-change/SKILL.md` (all agents) |
18
-
19
- **Forge-owned (never overwritten):** `/forge:apply` command, `forge/` skill tree.
1
+ # OpenSpec Forge overlays
2
+
3
+ Vendor OpenSpec skills and `/opsx:*` commands are upgraded in place. **Do not hand-edit** them.
4
+
5
+ Forge adaptations live here and are applied by:
6
+
7
+ ```bash
8
+ forge overlay
9
+ ```
10
+
11
+ Run after refreshing OpenSpec skills/commands from upstream (usually via `forge init --overlay`).
12
+
13
+ | Overlay | Targets |
14
+ | ------- | ------- |
15
+ | `opsx-apply-implement-step.md` | Step 6 in `opsx-apply` / `opsx/apply` commands |
16
+ | `opsx-apply-completion-step.md` | Step 7 “all done” bullet in same commands |
17
+ | `openspec-apply-change-footer.md` | Footer on `openspec-apply-change/SKILL.md` (all agents) |
18
+
19
+ **Forge-owned (never overwritten):** `/forge:apply` command, `forge/` skill tree.
@@ -1,14 +1,14 @@
1
-
2
- ---
3
-
4
- <!-- forgekit:openspec-overlay:start -->
5
-
6
- ## Forge overlay (re-applied by `forge overlay`)
7
-
8
- **Do not implement tasks inline in coordinator context.**
9
-
10
- 1. Prefer **`/forge:apply`** for OpenSpec implement + verify + review.
11
- 2. Otherwise follow vendor steps 1–5 above, then **`{{PHASES_IMPLEMENT}}`** for step 6+.
12
- 3. Re-apply this overlay after upgrading OpenSpec skills: `forge overlay`
13
-
14
- <!-- forgekit:openspec-overlay:end -->
1
+
2
+ ---
3
+
4
+ <!-- forgekit:openspec-overlay:start -->
5
+
6
+ ## Forge overlay (re-applied by `forge overlay`)
7
+
8
+ **Do not implement tasks inline in coordinator context.**
9
+
10
+ 1. Prefer **`/forge:apply`** for OpenSpec implement + verify + review.
11
+ 2. Otherwise follow vendor steps 1–5 above, then **`{{PHASES_IMPLEMENT}}`** for step 6+.
12
+ 3. Re-apply this overlay after upgrading OpenSpec skills: `forge overlay`
13
+
14
+ <!-- forgekit:openspec-overlay:end -->
@@ -1 +1 @@
1
- - If all done: proceed to Forge [verify]({{PHASES_VERIFY}}) and [review]({{PHASES_REVIEW}}) when in a Forge session (or user invoked `/forge:apply`), then suggest `/opsx:archive`
1
+ - If all done: proceed to Forge [verify]({{PHASES_VERIFY}}) and [review]({{PHASES_REVIEW}}) when in a Forge session (or user invoked `/forge:apply`), then suggest `/opsx:archive`
@@ -1,11 +1,11 @@
1
- **REQUIRED (Forge):** Wrap this loop in the implement phase —
2
- `{{PHASES_IMPLEMENT}}`.
3
- Use bundled `skills/subagent-driven-development` + `skills/test-driven-development` per task.
4
- Prefer **`/forge:apply`** over bare `/opsx:apply` — same OpenSpec CLI steps plus verify and review.
5
-
6
- For each pending task:
7
- - Show which task is being worked on
8
- - Dispatch **implementer** subagent (TDD first), then **spec** + **quality** reviewers
9
- - Keep changes minimal and focused
10
- - Mark task complete in the tasks file: `- [ ]` → `- [x]`
11
- - Continue to next task
1
+ **REQUIRED (Forge):** Wrap this loop in the implement phase —
2
+ `{{PHASES_IMPLEMENT}}`.
3
+ Use bundled `skills/subagent-driven-development` + `skills/test-driven-development` per task.
4
+ Prefer **`/forge:apply`** over bare `/opsx:apply` — same OpenSpec CLI steps plus verify and review.
5
+
6
+ For each pending task:
7
+ - Show which task is being worked on
8
+ - Dispatch **implementer** subagent (TDD first), then **spec** + **quality** reviewers
9
+ - Keep changes minimal and focused
10
+ - Mark task complete in the tasks file: `- [ ]` → `- [x]`
11
+ - Continue to next task