jumpstart-mode 1.1.12 → 1.1.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 (146) hide show
  1. package/.github/agents/jumpstart-adversary.agent.md +2 -1
  2. package/.github/agents/jumpstart-architect.agent.md +5 -6
  3. package/.github/agents/jumpstart-challenger.agent.md +2 -1
  4. package/.github/agents/jumpstart-devops.agent.md +2 -2
  5. package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
  6. package/.github/agents/jumpstart-maintenance.agent.md +1 -0
  7. package/.github/agents/jumpstart-performance.agent.md +1 -0
  8. package/.github/agents/jumpstart-pm.agent.md +1 -1
  9. package/.github/agents/jumpstart-refactor.agent.md +1 -0
  10. package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
  11. package/.github/agents/jumpstart-researcher.agent.md +1 -0
  12. package/.github/agents/jumpstart-retrospective.agent.md +1 -0
  13. package/.github/agents/jumpstart-reviewer.agent.md +2 -0
  14. package/.github/agents/jumpstart-scout.agent.md +1 -1
  15. package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
  16. package/.github/agents/jumpstart-security.agent.md +2 -1
  17. package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
  18. package/.github/workflows/quality.yml +19 -2
  19. package/.jumpstart/agents/analyst.md +38 -0
  20. package/.jumpstart/agents/architect.md +38 -0
  21. package/.jumpstart/agents/challenger.md +38 -0
  22. package/.jumpstart/agents/developer.md +41 -0
  23. package/.jumpstart/agents/pm.md +38 -0
  24. package/.jumpstart/agents/scout.md +33 -0
  25. package/.jumpstart/agents/ux-designer.md +4 -0
  26. package/.jumpstart/config.yaml +24 -0
  27. package/.jumpstart/schemas/timeline.schema.json +1 -0
  28. package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
  29. package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
  30. package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
  31. package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
  32. package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
  33. package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  34. package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  35. package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
  36. package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
  37. package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  38. package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
  39. package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
  40. package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
  41. package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
  42. package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
  43. package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
  44. package/.jumpstart/state/timeline.json +659 -0
  45. package/.jumpstart/usage-log.json +74 -3
  46. package/README.md +62 -1
  47. package/bin/cli.js +3217 -1
  48. package/bin/headless-runner.js +62 -2
  49. package/bin/lib/agent-checkpoint.js +168 -0
  50. package/bin/lib/ai-evaluation.js +104 -0
  51. package/bin/lib/ai-intake.js +152 -0
  52. package/bin/lib/ambiguity-heatmap.js +152 -0
  53. package/bin/lib/artifact-comparison.js +104 -0
  54. package/bin/lib/ast-edit-engine.js +157 -0
  55. package/bin/lib/backlog-sync.js +338 -0
  56. package/bin/lib/bcdr-planning.js +158 -0
  57. package/bin/lib/bidirectional-trace.js +199 -0
  58. package/bin/lib/branch-workflow.js +266 -0
  59. package/bin/lib/cab-output.js +119 -0
  60. package/bin/lib/chat-integration.js +122 -0
  61. package/bin/lib/ci-cd-integration.js +208 -0
  62. package/bin/lib/codebase-retrieval.js +125 -0
  63. package/bin/lib/collaboration.js +168 -0
  64. package/bin/lib/compliance-packs.js +213 -0
  65. package/bin/lib/context-chunker.js +128 -0
  66. package/bin/lib/context-onboarding.js +122 -0
  67. package/bin/lib/contract-first.js +124 -0
  68. package/bin/lib/cost-router.js +148 -0
  69. package/bin/lib/credential-boundary.js +155 -0
  70. package/bin/lib/data-classification.js +180 -0
  71. package/bin/lib/data-contracts.js +129 -0
  72. package/bin/lib/db-evolution.js +158 -0
  73. package/bin/lib/decision-conflicts.js +299 -0
  74. package/bin/lib/delivery-confidence.js +361 -0
  75. package/bin/lib/dependency-upgrade.js +153 -0
  76. package/bin/lib/design-system.js +133 -0
  77. package/bin/lib/deterministic-artifacts.js +151 -0
  78. package/bin/lib/diagram-studio.js +115 -0
  79. package/bin/lib/domain-ontology.js +140 -0
  80. package/bin/lib/ea-review-packet.js +151 -0
  81. package/bin/lib/enterprise-search.js +123 -0
  82. package/bin/lib/enterprise-templates.js +140 -0
  83. package/bin/lib/environment-promotion.js +220 -0
  84. package/bin/lib/estimation-studio.js +130 -0
  85. package/bin/lib/event-modeling.js +133 -0
  86. package/bin/lib/evidence-collector.js +179 -0
  87. package/bin/lib/finops-planner.js +182 -0
  88. package/bin/lib/fitness-functions.js +279 -0
  89. package/bin/lib/focus.js +448 -0
  90. package/bin/lib/governance-dashboard.js +165 -0
  91. package/bin/lib/guided-handoff.js +120 -0
  92. package/bin/lib/impact-analysis.js +190 -0
  93. package/bin/lib/incident-feedback.js +157 -0
  94. package/bin/lib/integrate.js +1 -1
  95. package/bin/lib/knowledge-graph.js +122 -0
  96. package/bin/lib/legacy-modernizer.js +160 -0
  97. package/bin/lib/migration-planner.js +144 -0
  98. package/bin/lib/model-governance.js +185 -0
  99. package/bin/lib/model-router.js +144 -0
  100. package/bin/lib/multi-repo.js +272 -0
  101. package/bin/lib/next-phase.js +53 -8
  102. package/bin/lib/ops-ownership.js +152 -0
  103. package/bin/lib/parallel-agents.js +257 -0
  104. package/bin/lib/pattern-library.js +115 -0
  105. package/bin/lib/persona-packs.js +99 -0
  106. package/bin/lib/plan-executor.js +366 -0
  107. package/bin/lib/platform-engineering.js +119 -0
  108. package/bin/lib/playback-summaries.js +126 -0
  109. package/bin/lib/policy-engine.js +240 -0
  110. package/bin/lib/portfolio-reporting.js +357 -0
  111. package/bin/lib/pr-package.js +197 -0
  112. package/bin/lib/project-memory.js +235 -0
  113. package/bin/lib/prompt-governance.js +130 -0
  114. package/bin/lib/promptless-mode.js +128 -0
  115. package/bin/lib/quality-graph.js +193 -0
  116. package/bin/lib/raci-matrix.js +188 -0
  117. package/bin/lib/refactor-planner.js +167 -0
  118. package/bin/lib/reference-architectures.js +304 -0
  119. package/bin/lib/release-readiness.js +171 -0
  120. package/bin/lib/repo-graph.js +262 -0
  121. package/bin/lib/requirements-baseline.js +358 -0
  122. package/bin/lib/risk-register.js +211 -0
  123. package/bin/lib/role-approval.js +249 -0
  124. package/bin/lib/role-views.js +142 -0
  125. package/bin/lib/root-cause-analysis.js +132 -0
  126. package/bin/lib/runtime-debugger.js +154 -0
  127. package/bin/lib/safe-rename.js +135 -0
  128. package/bin/lib/semantic-diff.js +335 -0
  129. package/bin/lib/sla-slo.js +210 -0
  130. package/bin/lib/spec-comments.js +147 -0
  131. package/bin/lib/spec-maturity.js +287 -0
  132. package/bin/lib/sre-integration.js +154 -0
  133. package/bin/lib/structured-elicitation.js +174 -0
  134. package/bin/lib/telemetry-feedback.js +118 -0
  135. package/bin/lib/test-generator.js +146 -0
  136. package/bin/lib/timeline.js +2 -1
  137. package/bin/lib/tool-bridge.js +107 -0
  138. package/bin/lib/tool-guardrails.js +139 -0
  139. package/bin/lib/tool-schemas.js +172 -3
  140. package/bin/lib/transcript-ingestion.js +150 -0
  141. package/bin/lib/vendor-risk.js +173 -0
  142. package/bin/lib/waiver-workflow.js +174 -0
  143. package/bin/lib/web-dashboard.js +126 -0
  144. package/bin/lib/workshop-mode.js +165 -0
  145. package/bin/lib/workstream-ownership.js +104 -0
  146. package/package.json +1 -1
@@ -0,0 +1,272 @@
1
+ /**
2
+ * multi-repo.js — Multi-Repo Program Orchestration
3
+ *
4
+ * Lets one initiative span frontend, backend, infra, data, and docs
5
+ * repos with shared specs, dependencies, and release plans.
6
+ *
7
+ * Usage:
8
+ * node bin/lib/multi-repo.js init|status|link|plan [options]
9
+ *
10
+ * State file: .jumpstart/state/multi-repo.json
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'multi-repo.json');
19
+
20
+ /**
21
+ * Load the multi-repo state from disk.
22
+ * @param {string} [stateFile]
23
+ * @returns {object}
24
+ */
25
+ function loadMultiRepoState(stateFile) {
26
+ const filePath = stateFile || DEFAULT_STATE_FILE;
27
+ if (!fs.existsSync(filePath)) {
28
+ return defaultMultiRepoState();
29
+ }
30
+ try {
31
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
32
+ } catch {
33
+ return defaultMultiRepoState();
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Default multi-repo state structure.
39
+ * @returns {object}
40
+ */
41
+ function defaultMultiRepoState() {
42
+ return {
43
+ version: '1.0.0',
44
+ program_name: null,
45
+ created_at: new Date().toISOString(),
46
+ last_updated: null,
47
+ repos: [],
48
+ shared_specs: [],
49
+ dependencies: [],
50
+ release_plan: {
51
+ milestones: [],
52
+ current_milestone: null
53
+ }
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Save the multi-repo state to disk.
59
+ * @param {object} state
60
+ * @param {string} [stateFile]
61
+ */
62
+ function saveMultiRepoState(state, stateFile) {
63
+ const filePath = stateFile || DEFAULT_STATE_FILE;
64
+ const dir = path.dirname(filePath);
65
+ if (!fs.existsSync(dir)) {
66
+ fs.mkdirSync(dir, { recursive: true });
67
+ }
68
+ state.last_updated = new Date().toISOString();
69
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
70
+ }
71
+
72
+ /**
73
+ * Initialize a multi-repo program.
74
+ * @param {string} programName
75
+ * @param {object} [options]
76
+ * @returns {object}
77
+ */
78
+ function initProgram(programName, options = {}) {
79
+ if (!programName || typeof programName !== 'string' || !programName.trim()) {
80
+ return { success: false, error: 'Program name is required' };
81
+ }
82
+
83
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
84
+ const state = defaultMultiRepoState();
85
+ state.program_name = programName.trim();
86
+
87
+ saveMultiRepoState(state, stateFile);
88
+
89
+ return {
90
+ success: true,
91
+ program_name: state.program_name,
92
+ state_file: stateFile,
93
+ message: `Program "${state.program_name}" initialized`
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Link a repo to the current program.
99
+ * @param {string} repoUrl - Git URL or local path of the repo.
100
+ * @param {string} role - Role of this repo: frontend|backend|infra|data|docs|other.
101
+ * @param {object} [options]
102
+ * @returns {object}
103
+ */
104
+ function linkRepo(repoUrl, role, options = {}) {
105
+ if (!repoUrl) return { success: false, error: 'repoUrl is required' };
106
+
107
+ const validRoles = ['frontend', 'backend', 'infra', 'data', 'docs', 'other'];
108
+ const normalizedRole = (role || 'other').toLowerCase();
109
+ if (!validRoles.includes(normalizedRole)) {
110
+ return { success: false, error: `role must be one of: ${validRoles.join(', ')}` };
111
+ }
112
+
113
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
114
+ const state = loadMultiRepoState(stateFile);
115
+
116
+ const existing = state.repos.find(r => r.url === repoUrl);
117
+ if (existing) {
118
+ return { success: false, error: `Repo already linked: ${repoUrl}` };
119
+ }
120
+
121
+ const entry = {
122
+ id: `repo-${Date.now()}`,
123
+ url: repoUrl,
124
+ role: normalizedRole,
125
+ linked_at: new Date().toISOString(),
126
+ specs: [],
127
+ status: 'active'
128
+ };
129
+
130
+ state.repos.push(entry);
131
+ saveMultiRepoState(state, stateFile);
132
+
133
+ return {
134
+ success: true,
135
+ repo: entry,
136
+ total_repos: state.repos.length
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Add a shared spec reference across repos.
142
+ * @param {string} specPath - Relative path to shared spec file.
143
+ * @param {string[]} repoIds - IDs of repos this spec applies to.
144
+ * @param {object} [options]
145
+ * @returns {object}
146
+ */
147
+ function addSharedSpec(specPath, repoIds, options = {}) {
148
+ if (!specPath) return { success: false, error: 'specPath is required' };
149
+
150
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
151
+ const state = loadMultiRepoState(stateFile);
152
+
153
+ const entry = {
154
+ id: `spec-${Date.now()}`,
155
+ path: specPath,
156
+ repos: Array.isArray(repoIds) ? repoIds : [],
157
+ added_at: new Date().toISOString()
158
+ };
159
+
160
+ state.shared_specs.push(entry);
161
+ saveMultiRepoState(state, stateFile);
162
+
163
+ return {
164
+ success: true,
165
+ spec: entry,
166
+ total_shared_specs: state.shared_specs.length
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Record a cross-repo dependency.
172
+ * @param {string} fromRepoId - Source repo ID.
173
+ * @param {string} toRepoId - Target repo ID.
174
+ * @param {string} dependencyType - Type: api|data|event|deploy|other.
175
+ * @param {object} [options]
176
+ * @returns {object}
177
+ */
178
+ function addDependency(fromRepoId, toRepoId, dependencyType, options = {}) {
179
+ if (!fromRepoId || !toRepoId) {
180
+ return { success: false, error: 'fromRepoId and toRepoId are required' };
181
+ }
182
+
183
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
184
+ const state = loadMultiRepoState(stateFile);
185
+
186
+ const dep = {
187
+ id: `dep-${Date.now()}`,
188
+ from: fromRepoId,
189
+ to: toRepoId,
190
+ type: dependencyType || 'other',
191
+ created_at: new Date().toISOString()
192
+ };
193
+
194
+ state.dependencies.push(dep);
195
+ saveMultiRepoState(state, stateFile);
196
+
197
+ return { success: true, dependency: dep };
198
+ }
199
+
200
+ /**
201
+ * Get the current status of the multi-repo program.
202
+ * @param {object} [options]
203
+ * @returns {object}
204
+ */
205
+ function getProgramStatus(options = {}) {
206
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
207
+ const state = loadMultiRepoState(stateFile);
208
+
209
+ const roleBreakdown = {};
210
+ for (const repo of state.repos) {
211
+ roleBreakdown[repo.role] = (roleBreakdown[repo.role] || 0) + 1;
212
+ }
213
+
214
+ return {
215
+ program_name: state.program_name,
216
+ initialized: !!state.program_name,
217
+ repo_count: state.repos.length,
218
+ shared_spec_count: state.shared_specs.length,
219
+ dependency_count: state.dependencies.length,
220
+ role_breakdown: roleBreakdown,
221
+ repos: state.repos,
222
+ release_plan: state.release_plan,
223
+ last_updated: state.last_updated
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Generate a release plan across all linked repos.
229
+ * @param {object[]} milestones - Array of { name, target_date, repos[] }.
230
+ * @param {object} [options]
231
+ * @returns {object}
232
+ */
233
+ function setReleasePlan(milestones, options = {}) {
234
+ if (!Array.isArray(milestones)) {
235
+ return { success: false, error: 'milestones must be an array' };
236
+ }
237
+
238
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
239
+ const state = loadMultiRepoState(stateFile);
240
+
241
+ state.release_plan.milestones = milestones.map((m, i) => ({
242
+ id: `milestone-${i + 1}`,
243
+ name: m.name || `Milestone ${i + 1}`,
244
+ target_date: m.target_date || null,
245
+ repos: m.repos || [],
246
+ status: m.status || 'planned'
247
+ }));
248
+
249
+ if (state.release_plan.milestones.length > 0) {
250
+ state.release_plan.current_milestone = state.release_plan.milestones[0].id;
251
+ }
252
+
253
+ saveMultiRepoState(state, stateFile);
254
+
255
+ return {
256
+ success: true,
257
+ milestone_count: state.release_plan.milestones.length,
258
+ release_plan: state.release_plan
259
+ };
260
+ }
261
+
262
+ module.exports = {
263
+ loadMultiRepoState,
264
+ saveMultiRepoState,
265
+ defaultMultiRepoState,
266
+ initProgram,
267
+ linkRepo,
268
+ addSharedSpec,
269
+ addDependency,
270
+ getProgramStatus,
271
+ setReleasePlan
272
+ };
@@ -31,6 +31,7 @@ const { join, resolve } = require('path');
31
31
  import { loadState } from './state-store.js';
32
32
  import { getHandoff, isArtifactApproved } from './handoff.js';
33
33
  import { parseSimpleYaml } from './config-loader.js';
34
+ import { readFocusFromConfig, isPhaseInFocus } from './focus.js';
34
35
 
35
36
  /**
36
37
  * Phase-to-slash-command map.
@@ -121,6 +122,9 @@ export function determineNextAction(options = {}) {
121
122
  const projectType = config?.project?.type || 'greenfield';
122
123
  const requireApproval = config?.workflow?.require_gate_approval !== false;
123
124
 
125
+ // Load focus mode config (if active)
126
+ const focusConfig = readFocusFromConfig(configPath);
127
+
124
128
  // Load current state
125
129
  const state = loadState(statePath);
126
130
  const currentPhase = state.current_phase;
@@ -142,7 +146,8 @@ export function determineNextAction(options = {}) {
142
146
  next_agent: 'analyst',
143
147
  command: AGENT_COMMANDS.analyst,
144
148
  message: 'Phase 0 (Challenger) is already approved. Next: Phase 1 — ' + PHASE_DESCRIPTIONS['1'],
145
- context_files: getHandoff(0).context_files || []
149
+ context_files: getHandoff(0).context_files || [],
150
+ focus: focusConfig && focusConfig.enabled ? { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase } : undefined
146
151
  };
147
152
  }
148
153
  return {
@@ -153,7 +158,26 @@ export function determineNextAction(options = {}) {
153
158
  command: '/jumpstart.review',
154
159
  artifact: 'specs/challenger-brief.md',
155
160
  message: 'Phase 0 (Challenger) artifact exists but is not yet approved. Review and approve it to proceed.',
156
- context_files: []
161
+ context_files: [],
162
+ focus: focusConfig && focusConfig.enabled ? { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase } : undefined
163
+ };
164
+ }
165
+
166
+ // If focus mode is active, skip to the focus start phase
167
+ if (focusConfig && focusConfig.enabled) {
168
+ const focusStart = focusConfig.start_phase;
169
+ const agentNames = { '-1': 'scout', '0': 'challenger', '1': 'analyst', '2': 'pm', '3': 'architect', '4': 'developer' };
170
+ const agent = agentNames[String(focusStart)];
171
+ const command = AGENT_COMMANDS[agent];
172
+ return {
173
+ action: 'start',
174
+ current_phase: null,
175
+ next_phase: focusStart,
176
+ next_agent: agent,
177
+ command: command,
178
+ message: `Focus mode active (${focusConfig.preset || 'custom'}). Start with Phase ${focusStart} — ${PHASE_DESCRIPTIONS[String(focusStart)]}`,
179
+ context_files: [],
180
+ focus: { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase }
157
181
  };
158
182
  }
159
183
 
@@ -206,17 +230,23 @@ export function determineNextAction(options = {}) {
206
230
  };
207
231
  }
208
232
 
209
- // ─── Case 2: Final phase (4) — check completion ───────────────────────────
210
- if (currentPhase === 4) {
233
+ // ─── Case 2: Final phase or focus end reached — check completion ─────────
234
+ if (currentPhase === 4 || (focusConfig && focusConfig.enabled && currentPhase > focusConfig.end_phase)) {
235
+ const focusNote = focusConfig && focusConfig.enabled
236
+ ? ` Focus mode (${focusConfig.preset || 'custom'}) workflow complete.`
237
+ : '';
211
238
  return {
212
239
  action: 'complete',
213
- current_phase: 4,
240
+ current_phase: currentPhase,
214
241
  next_phase: null,
215
242
  next_agent: null,
216
243
  command: '/jumpstart.status',
217
- message: 'Phase 4 (Developer) is the final phase. All specification phases are complete. Run `/jumpstart.status` for a full project overview, or `/jumpstart.deploy` for deployment planning.',
244
+ message: currentPhase === 4
245
+ ? 'Phase 4 (Developer) is the final phase. All specification phases are complete. Run `/jumpstart.status` for a full project overview, or `/jumpstart.deploy` for deployment planning.'
246
+ : `Phase ${currentPhase} (${PHASE_NAMES[String(currentPhase)]}) reached the end of focus range.${focusNote} Run \`/jumpstart.status\` for a project overview.`,
218
247
  suggestions: ['/jumpstart.status', '/jumpstart.deploy', '/jumpstart.resume'],
219
- context_files: []
248
+ context_files: [],
249
+ focus: focusConfig && focusConfig.enabled ? { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase } : undefined
220
250
  };
221
251
  }
222
252
 
@@ -278,6 +308,20 @@ export function determineNextAction(options = {}) {
278
308
  const nextPhase = handoff.next_phase;
279
309
  const command = AGENT_COMMANDS[nextAgent];
280
310
 
311
+ // Check if next phase is beyond focus range
312
+ if (focusConfig && focusConfig.enabled && !isPhaseInFocus(nextPhase, focusConfig)) {
313
+ return {
314
+ action: 'complete',
315
+ current_phase: currentPhase,
316
+ next_phase: null,
317
+ next_agent: null,
318
+ command: '/jumpstart.status',
319
+ message: `Phase ${currentPhase} (${PHASE_NAMES[String(currentPhase)]}) is approved! Focus mode (${focusConfig.preset || 'custom'}) workflow complete — Phase ${nextPhase} is outside the focus range. Run \`/jumpstart.status\` for a project overview.`,
320
+ context_files: [],
321
+ focus: { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase }
322
+ };
323
+ }
324
+
281
325
  return {
282
326
  action: 'proceed',
283
327
  current_phase: currentPhase,
@@ -285,7 +329,8 @@ export function determineNextAction(options = {}) {
285
329
  next_agent: nextAgent,
286
330
  command: command,
287
331
  message: `Phase ${currentPhase} (${PHASE_NAMES[String(currentPhase)]}) is approved! Next: Phase ${nextPhase} — ${PHASE_DESCRIPTIONS[String(nextPhase)]}`,
288
- context_files: handoff.context_files || []
332
+ context_files: handoff.context_files || [],
333
+ focus: focusConfig && focusConfig.enabled ? { active: true, start_phase: focusConfig.start_phase, end_phase: focusConfig.end_phase } : undefined
289
334
  };
290
335
  }
291
336
 
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ops-ownership.js — Operational Ownership Modeling (Item 39)
3
+ *
4
+ * Require clear service owner, escalation path, on-call model,
5
+ * and support plan.
6
+ *
7
+ * Usage:
8
+ * node bin/lib/ops-ownership.js define|check|report [options]
9
+ *
10
+ * State file: .jumpstart/state/ops-ownership.json
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'ops-ownership.json');
19
+
20
+ const OWNERSHIP_FIELDS = ['service_owner', 'team', 'escalation_path', 'oncall_model', 'support_hours', 'runbook_url', 'sla_tier'];
21
+
22
+ function defaultState() {
23
+ return {
24
+ version: '1.0.0',
25
+ created_at: new Date().toISOString(),
26
+ last_updated: null,
27
+ services: []
28
+ };
29
+ }
30
+
31
+ function loadState(stateFile) {
32
+ const filePath = stateFile || DEFAULT_STATE_FILE;
33
+ if (!fs.existsSync(filePath)) return defaultState();
34
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
35
+ catch { return defaultState(); }
36
+ }
37
+
38
+ function saveState(state, stateFile) {
39
+ const filePath = stateFile || DEFAULT_STATE_FILE;
40
+ const dir = path.dirname(filePath);
41
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
42
+ state.last_updated = new Date().toISOString();
43
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
44
+ }
45
+
46
+ /**
47
+ * Define operational ownership for a service.
48
+ *
49
+ * @param {object} service - { name, service_owner, team, escalation_path[], oncall_model, support_hours, runbook_url?, sla_tier? }
50
+ * @param {object} [options]
51
+ * @returns {object}
52
+ */
53
+ function defineOwnership(service, options = {}) {
54
+ if (!service || !service.name || !service.service_owner) {
55
+ return { success: false, error: 'name and service_owner are required' };
56
+ }
57
+
58
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
59
+ const state = loadState(stateFile);
60
+
61
+ const svc = {
62
+ id: `OPS-${(state.services.length + 1).toString().padStart(3, '0')}`,
63
+ name: service.name,
64
+ service_owner: service.service_owner,
65
+ team: service.team || null,
66
+ escalation_path: service.escalation_path || [],
67
+ oncall_model: service.oncall_model || 'business-hours',
68
+ support_hours: service.support_hours || '9x5',
69
+ runbook_url: service.runbook_url || null,
70
+ sla_tier: service.sla_tier || 'silver',
71
+ defined_at: new Date().toISOString()
72
+ };
73
+
74
+ // Replace if already exists
75
+ const idx = state.services.findIndex(s => s.name === service.name);
76
+ if (idx >= 0) state.services[idx] = svc;
77
+ else state.services.push(svc);
78
+
79
+ saveState(state, stateFile);
80
+
81
+ return { success: true, service: svc };
82
+ }
83
+
84
+ /**
85
+ * Check ownership completeness.
86
+ *
87
+ * @param {object} [options]
88
+ * @returns {object}
89
+ */
90
+ function checkCompleteness(options = {}) {
91
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
92
+ const state = loadState(stateFile);
93
+
94
+ const incomplete = [];
95
+ for (const svc of state.services) {
96
+ const missing = [];
97
+ if (!svc.service_owner) missing.push('service_owner');
98
+ if (!svc.team) missing.push('team');
99
+ if (!svc.escalation_path || svc.escalation_path.length === 0) missing.push('escalation_path');
100
+ if (!svc.runbook_url) missing.push('runbook_url');
101
+
102
+ if (missing.length > 0) {
103
+ incomplete.push({ service: svc.name, missing });
104
+ }
105
+ }
106
+
107
+ return {
108
+ success: true,
109
+ total_services: state.services.length,
110
+ complete: state.services.length - incomplete.length,
111
+ incomplete: incomplete.length,
112
+ findings: incomplete,
113
+ all_complete: incomplete.length === 0
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Generate ops ownership report.
119
+ *
120
+ * @param {object} [options]
121
+ * @returns {object}
122
+ */
123
+ function generateReport(options = {}) {
124
+ const stateFile = options.stateFile || DEFAULT_STATE_FILE;
125
+ const state = loadState(stateFile);
126
+
127
+ const byTeam = {};
128
+ const byTier = {};
129
+ for (const svc of state.services) {
130
+ const team = svc.team || 'unassigned';
131
+ byTeam[team] = (byTeam[team] || 0) + 1;
132
+ byTier[svc.sla_tier] = (byTier[svc.sla_tier] || 0) + 1;
133
+ }
134
+
135
+ return {
136
+ success: true,
137
+ total_services: state.services.length,
138
+ by_team: byTeam,
139
+ by_tier: byTier,
140
+ services: state.services
141
+ };
142
+ }
143
+
144
+ module.exports = {
145
+ defaultState,
146
+ loadState,
147
+ saveState,
148
+ defineOwnership,
149
+ checkCompleteness,
150
+ generateReport,
151
+ OWNERSHIP_FIELDS
152
+ };