@n12r/jarvis-control 0.2.3

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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Jarvis Control Plane
2
+
3
+ Deterministic workflow gates for AI-assisted software delivery.
4
+
5
+ ## Commands
6
+
7
+ - `jarvis-control init` - scaffold workflow files into the current project.
8
+ - `jarvis-control status` - show current stage and gate status.
9
+ - `jarvis-control check [stage]` - run gate checks for current or specified stage.
10
+ - `jarvis-control promote <target-stage>` - move to next stage if current stage passes.
11
+ - `jarvis-control repo [owner repo-name --public|--private --template owner/repo]` - create and clone a new repo (prompts interactively if args are omitted).
12
+
13
+ ## Local usage
14
+
15
+ ```bash
16
+ npm install
17
+ npm run cp:init
18
+ npm run cp:status
19
+ npm run cp:check
20
+ ```
21
+
22
+ ## Stage model
23
+
24
+ `discovery -> contract -> plan -> build -> harden -> release -> operate`
25
+
26
+ Gate requirements are defined in `workflow/gates.yaml`.
27
+ Workflow state is tracked in `workflow/state.yaml`.
28
+ Transition history is tracked in `workflow/history.yaml`.
29
+ Note: `workflow/` is runtime state and is intentionally not committed in this template repo.
30
+ Run `jarvis-control init` in each new repo to scaffold it.
31
+
32
+ ## How this controls AI-assisted work
33
+
34
+ 1. AI can generate code and docs.
35
+ 2. Promotion between stages is blocked until deterministic checks pass.
36
+ 3. Stage transitions are chain-validated from history to block stage jumps.
37
+ 4. CI runs the same checks to prevent bypass.
38
+
39
+ ## Reuse Across New Repos
40
+
41
+ 1. Mark this repo as a GitHub Template Repository.
42
+ 2. Create a new repo from template:
43
+ - `jarvis-control repo <owner> <new-repo-name> --private`
44
+ - From any directory: `jarvis-control repo <owner> <new-repo-name> --private --template <owner/repo>`
45
+ - Optional env default: `export JARVIS_TEMPLATE_REPO=<owner/repo>`
46
+ 3. Apply branch and environment protections:
47
+ - `scripts/apply-github-protections.sh <owner> <repo> main`
48
+ 4. Follow the full checklist:
49
+ - `docs/REPO_BOOTSTRAP_CHECKLIST.md`
50
+
51
+ ## Scripts
52
+
53
+ - `scripts/create-repo-from-template.sh` - create/clone repos from this template.
54
+ - `scripts/apply-github-protections.sh` - enforce branch/environment protection via GitHub API.
55
+ - `scripts/guard-state-change.js` - CI guard to block invalid `state.yaml` edits.
56
+
57
+ ## Next steps
58
+
59
+ 1. Publish this package to npm.
60
+ 2. Use `npx jarvis-control@<version> init` in any project.
61
+ 3. Customize `workflow/gates.yaml` per project domain.
package/bin/jarvis.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ const { init, status, check, promote, repo } = require('../lib/control-plane');
3
+ const readline = require('readline');
4
+ const CLI_NAME = 'jarvis-control';
5
+
6
+ const RESET = '\x1b[0m';
7
+ const BOLD = '\x1b[1m';
8
+ const CYAN = '\x1b[36m';
9
+ const GREEN = '\x1b[32m';
10
+ const YELLOW = '\x1b[33m';
11
+
12
+ function printHelp() {
13
+ console.log(`${BOLD}${CYAN}Jarvis Control Plane${RESET}`);
14
+ console.log(`${YELLOW}Deterministic workflow gates for AI-assisted delivery${RESET}\n`);
15
+
16
+ console.log(`${BOLD}Quick Start${RESET}`);
17
+ console.log(` ${GREEN}1)${RESET} ${CLI_NAME} init`);
18
+ console.log(` ${GREEN}2)${RESET} ${CLI_NAME} status`);
19
+ console.log(` ${GREEN}3)${RESET} ${CLI_NAME} check`);
20
+ console.log(` ${GREEN}4)${RESET} ${CLI_NAME} promote <stage>\n`);
21
+
22
+ console.log(`${BOLD}Commands${RESET}`);
23
+ console.log(` ${BOLD}init${RESET} Scaffold ./workflow files`);
24
+ console.log(` ${BOLD}status${RESET} Show current stage + gate status`);
25
+ console.log(` ${BOLD}check [stage]${RESET} Run gate checks for current or provided stage`);
26
+ console.log(` ${BOLD}promote <stage>${RESET} Move to next stage if current stage passes`);
27
+ console.log(` ${BOLD}repo [owner name]${RESET} Create and clone a repo from template (wizard if omitted)`);
28
+ console.log(` ${BOLD}help${RESET} Show this menu\n`);
29
+
30
+ console.log(`${BOLD}Stage Flow${RESET}`);
31
+ console.log(' discovery -> contract -> plan -> build -> harden -> release -> operate\n');
32
+
33
+ console.log(`${BOLD}Examples${RESET}`);
34
+ console.log(` ${CLI_NAME} check`);
35
+ console.log(` ${CLI_NAME} check build`);
36
+ console.log(` ${CLI_NAME} promote contract`);
37
+ console.log(` ${CLI_NAME} repo`);
38
+ console.log(` ${CLI_NAME} repo schradermade my-new-service --private --template schradermade/jarvis`);
39
+ console.log('\nRepo flags:');
40
+ console.log(' --template <owner/repo> Explicit template repo (works from any directory)');
41
+ console.log(' --private | --public Visibility');
42
+ console.log(' Env fallback: JARVIS_TEMPLATE_REPO=<owner/repo>');
43
+ }
44
+
45
+ function prompt(question) {
46
+ return new Promise((resolve) => {
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ });
51
+ rl.question(question, (answer) => {
52
+ rl.close();
53
+ resolve((answer || '').trim());
54
+ });
55
+ });
56
+ }
57
+
58
+ async function resolveRepoArgs(args) {
59
+ const positional = [];
60
+ let visibility = '';
61
+ let template = '';
62
+
63
+ for (let i = 0; i < args.length; i += 1) {
64
+ const token = args[i];
65
+ if (token === '--template') {
66
+ template = args[i + 1] || '';
67
+ i += 1;
68
+ continue;
69
+ }
70
+ if (token === '--private' || token === '--public' || token === '1' || token === '2') {
71
+ visibility = token;
72
+ continue;
73
+ }
74
+ positional.push(token);
75
+ }
76
+
77
+ let owner = positional[0];
78
+ let repoName = positional[1];
79
+ template = template || process.env.JARVIS_TEMPLATE_REPO || '';
80
+
81
+ if (owner && repoName) {
82
+ return { owner, repoName, visibility: visibility || '--private', template };
83
+ }
84
+
85
+ if (!process.stdin.isTTY) {
86
+ throw new Error('repo command requires args in non-interactive mode: <owner> <repo-name> [--public|--private]');
87
+ }
88
+
89
+ console.log(`${BOLD}${CYAN}Repo Setup Wizard${RESET}`);
90
+ console.log(`${YELLOW}Create and clone a new repo from this template${RESET}\n`);
91
+
92
+ owner = owner || (await prompt('GitHub owner/org: '));
93
+ repoName = repoName || (await prompt('New repository name: '));
94
+ template = template || (await prompt('Template repo (owner/repo) [auto-detect from current repo]: '));
95
+ const visibilityInput =
96
+ visibility ||
97
+ (await prompt('Visibility (1=private, 2=public) [1]: ')) ||
98
+ '1';
99
+
100
+ if (visibilityInput === '1') {
101
+ visibility = '--private';
102
+ } else if (visibilityInput === '2') {
103
+ visibility = '--public';
104
+ } else if (visibilityInput === '--private' || visibilityInput === '--public') {
105
+ visibility = visibilityInput;
106
+ } else {
107
+ throw new Error("Visibility must be '1' (private), '2' (public), '--private', or '--public'");
108
+ }
109
+
110
+ return { owner, repoName, visibility, template };
111
+ }
112
+
113
+ async function main() {
114
+ const [, , command, ...args] = process.argv;
115
+
116
+ try {
117
+ switch (command) {
118
+ case 'init':
119
+ process.exitCode = init();
120
+ break;
121
+ case 'status':
122
+ process.exitCode = status();
123
+ break;
124
+ case 'check':
125
+ process.exitCode = check(args[0]);
126
+ break;
127
+ case 'promote':
128
+ process.exitCode = promote(args[0]);
129
+ break;
130
+ case 'repo':
131
+ {
132
+ const repoArgs = await resolveRepoArgs(args);
133
+ process.exitCode = repo(
134
+ repoArgs.owner,
135
+ repoArgs.repoName,
136
+ repoArgs.visibility,
137
+ repoArgs.template,
138
+ );
139
+ }
140
+ break;
141
+ case 'help':
142
+ case '--help':
143
+ case '-h':
144
+ printHelp();
145
+ process.exitCode = 0;
146
+ break;
147
+ default:
148
+ if (!command) {
149
+ printHelp();
150
+ process.exitCode = 0;
151
+ } else {
152
+ console.error(`Unknown command: ${command}\n`);
153
+ printHelp();
154
+ process.exitCode = 1;
155
+ }
156
+ }
157
+ } catch (error) {
158
+ console.error(error.message);
159
+ console.log('');
160
+ console.log(`${YELLOW}Run '${CLI_NAME} help' for command usage.${RESET}`);
161
+ process.exitCode = 1;
162
+ }
163
+ }
164
+
165
+ main();
@@ -0,0 +1,422 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+ const yaml = require('js-yaml');
5
+
6
+ const WORKFLOW_DIR = path.join(process.cwd(), 'workflow');
7
+ const STATE_PATH = path.join(WORKFLOW_DIR, 'state.yaml');
8
+ const GATES_PATH = path.join(WORKFLOW_DIR, 'gates.yaml');
9
+ const HISTORY_PATH = path.join(WORKFLOW_DIR, 'history.yaml');
10
+ const RESET = '\x1b[0m';
11
+ const BOLD = '\x1b[1m';
12
+ const DIM = '\x1b[2m';
13
+ const GREEN = '\x1b[32m';
14
+ const CYAN = '\x1b[36m';
15
+ const YELLOW = '\x1b[33m';
16
+
17
+ function fileExists(filePath) {
18
+ return fs.existsSync(filePath);
19
+ }
20
+
21
+ function ensureWorkflowFiles() {
22
+ if (!fileExists(STATE_PATH) || !fileExists(GATES_PATH) || !fileExists(HISTORY_PATH)) {
23
+ throw new Error('Missing workflow files. Run `jarvis-control init` first.');
24
+ }
25
+ }
26
+
27
+ function loadYaml(filePath) {
28
+ return yaml.load(fs.readFileSync(filePath, 'utf8'));
29
+ }
30
+
31
+ function saveYaml(filePath, data) {
32
+ const content = yaml.dump(data, { lineWidth: 120, noRefs: true });
33
+ fs.writeFileSync(filePath, content, 'utf8');
34
+ }
35
+
36
+ function validateHistory(state, gates, history) {
37
+ if (!history || !Array.isArray(history.transitions)) {
38
+ throw new Error('Invalid workflow/history.yaml: missing transitions array');
39
+ }
40
+
41
+ const transitions = history.transitions;
42
+ if (transitions.length === 0) {
43
+ throw new Error('Invalid workflow/history.yaml: transitions must contain at least one entry');
44
+ }
45
+
46
+ let previousStage = null;
47
+ for (let i = 0; i < transitions.length; i += 1) {
48
+ const transition = transitions[i];
49
+ const from = transition.from ?? null;
50
+ const to = transition.to;
51
+
52
+ if (!to || !gates.stages[to]) {
53
+ throw new Error(`Invalid transition at index ${i}: unknown target stage '${to}'`);
54
+ }
55
+
56
+ if (i === 0) {
57
+ if (from !== null) {
58
+ throw new Error('Invalid transition history: first transition must have from: null');
59
+ }
60
+ previousStage = to;
61
+ continue;
62
+ }
63
+
64
+ if (from !== previousStage) {
65
+ throw new Error(`Invalid transition history at index ${i}: from '${from}' does not match previous stage '${previousStage}'`);
66
+ }
67
+
68
+ const allowedNext = gates.stages[from].allowed_next || [];
69
+ if (!allowedNext.includes(to)) {
70
+ throw new Error(`Invalid transition history at index ${i}: '${from}' cannot transition to '${to}'`);
71
+ }
72
+
73
+ previousStage = to;
74
+ }
75
+
76
+ const historyCurrent = transitions[transitions.length - 1].to;
77
+ if (historyCurrent !== state.current_stage) {
78
+ throw new Error(
79
+ `State/history mismatch: state current_stage is '${state.current_stage}' but history ends at '${historyCurrent}'`,
80
+ );
81
+ }
82
+ }
83
+
84
+ function getStateAndGates() {
85
+ ensureWorkflowFiles();
86
+ const state = loadYaml(STATE_PATH);
87
+ const gates = loadYaml(GATES_PATH);
88
+ const history = loadYaml(HISTORY_PATH);
89
+ if (!state || !state.current_stage) {
90
+ throw new Error('Invalid workflow/state.yaml: missing current_stage');
91
+ }
92
+ if (!gates || !gates.stages || typeof gates.stages !== 'object') {
93
+ throw new Error('Invalid workflow/gates.yaml: missing stages');
94
+ }
95
+ validateHistory(state, gates, history);
96
+ return { state, gates, history };
97
+ }
98
+
99
+ function missingFiles(requiredFiles) {
100
+ return requiredFiles.filter((f) => !fileExists(path.join(process.cwd(), f)));
101
+ }
102
+
103
+ function runCommands(requiredCommands) {
104
+ const failures = [];
105
+ for (const cmd of requiredCommands) {
106
+ const result = spawnSync(cmd, {
107
+ shell: true,
108
+ stdio: 'inherit',
109
+ cwd: process.cwd(),
110
+ });
111
+ if (result.status !== 0) {
112
+ failures.push(cmd);
113
+ }
114
+ }
115
+ return failures;
116
+ }
117
+
118
+ function checkStage(stageName) {
119
+ const { gates } = getStateAndGates();
120
+ const stage = gates.stages[stageName];
121
+ if (!stage) {
122
+ throw new Error(`Unknown stage: ${stageName}`);
123
+ }
124
+
125
+ const requiredFiles = stage.required_files || [];
126
+ const requiredCommands = stage.required_commands || [];
127
+
128
+ const filesMissing = missingFiles(requiredFiles);
129
+ const commandFailures = runCommands(requiredCommands);
130
+
131
+ const ok = filesMissing.length === 0 && commandFailures.length === 0;
132
+ return {
133
+ ok,
134
+ stage: stageName,
135
+ filesMissing,
136
+ commandFailures,
137
+ };
138
+ }
139
+
140
+ function findStartStage(gates) {
141
+ const stageNames = Object.keys(gates.stages);
142
+ const hasIncoming = new Set();
143
+ for (const name of stageNames) {
144
+ const next = gates.stages[name].allowed_next || [];
145
+ next.forEach((n) => hasIncoming.add(n));
146
+ }
147
+ return stageNames.find((name) => !hasIncoming.has(name));
148
+ }
149
+
150
+ function buildStagePath(gates) {
151
+ const start = findStartStage(gates);
152
+ if (!start) {
153
+ return Object.keys(gates.stages);
154
+ }
155
+ const path = [];
156
+ const seen = new Set();
157
+ let current = start;
158
+ while (current && !seen.has(current)) {
159
+ path.push(current);
160
+ seen.add(current);
161
+ const next = gates.stages[current].allowed_next || [];
162
+ current = next[0];
163
+ }
164
+ return path.length ? path : Object.keys(gates.stages);
165
+ }
166
+
167
+ function renderStageProgress(currentStage, gates) {
168
+ const path = buildStagePath(gates);
169
+ const currentIdx = path.indexOf(currentStage);
170
+ const parts = path.map((stage, idx) => {
171
+ if (idx < currentIdx) {
172
+ return `${GREEN}✔ ${stage}${RESET}`;
173
+ }
174
+ if (idx === currentIdx) {
175
+ return `${BOLD}${CYAN}➤ ${stage}${RESET}`;
176
+ }
177
+ return `${DIM}• ${stage}${RESET}`;
178
+ });
179
+
180
+ const flow = parts.join(`${DIM} → ${RESET}`);
181
+ console.log(`${BOLD}Stage Pipeline${RESET}`);
182
+ console.log(flow);
183
+ console.log('');
184
+ }
185
+
186
+ function status() {
187
+ const { state, gates } = getStateAndGates();
188
+ const result = checkStage(state.current_stage);
189
+
190
+ renderStageProgress(state.current_stage, gates);
191
+ console.log(`Current stage: ${state.current_stage}`);
192
+ if (state.owner) {
193
+ console.log(`Owner: ${state.owner}`);
194
+ }
195
+ if (state.last_updated) {
196
+ console.log(`Last updated: ${state.last_updated}`);
197
+ }
198
+
199
+ if (result.ok) {
200
+ console.log(`${GREEN}Gate status: PASS${RESET}`);
201
+ return 0;
202
+ }
203
+
204
+ console.log(`${YELLOW}Gate status: FAIL${RESET}`);
205
+ if (result.filesMissing.length) {
206
+ console.log('Missing files:');
207
+ result.filesMissing.forEach((f) => console.log(`- ${f}`));
208
+ }
209
+ if (result.commandFailures.length) {
210
+ console.log('Failed commands:');
211
+ result.commandFailures.forEach((c) => console.log(`- ${c}`));
212
+ }
213
+
214
+ return 1;
215
+ }
216
+
217
+ function check(optionalStage) {
218
+ const { state } = getStateAndGates();
219
+ const stage = optionalStage || state.current_stage;
220
+ const result = checkStage(stage);
221
+
222
+ console.log(`Checking stage: ${stage}`);
223
+ if (result.ok) {
224
+ console.log('PASS');
225
+ return 0;
226
+ }
227
+
228
+ console.log('FAIL');
229
+ if (result.filesMissing.length) {
230
+ console.log('Missing files:');
231
+ result.filesMissing.forEach((f) => console.log(`- ${f}`));
232
+ }
233
+ if (result.commandFailures.length) {
234
+ console.log('Failed commands:');
235
+ result.commandFailures.forEach((c) => console.log(`- ${c}`));
236
+ }
237
+
238
+ return 1;
239
+ }
240
+
241
+ function promote(targetStage) {
242
+ if (!targetStage) {
243
+ throw new Error('Usage: jarvis-control promote <target-stage>');
244
+ }
245
+
246
+ const { state, gates, history } = getStateAndGates();
247
+ const current = state.current_stage;
248
+ const currentConfig = gates.stages[current];
249
+ const targetConfig = gates.stages[targetStage];
250
+
251
+ if (!currentConfig) {
252
+ throw new Error(`Current stage '${current}' is not defined in gates.yaml`);
253
+ }
254
+ if (!targetConfig) {
255
+ throw new Error(`Target stage '${targetStage}' is not defined in gates.yaml`);
256
+ }
257
+
258
+ const allowedNext = currentConfig.allowed_next || [];
259
+ if (!allowedNext.includes(targetStage)) {
260
+ throw new Error(`Cannot promote from '${current}' to '${targetStage}'. Allowed next: ${allowedNext.join(', ') || '(none)'}`);
261
+ }
262
+
263
+ const gate = checkStage(current);
264
+ if (!gate.ok) {
265
+ console.log(`Cannot promote: current stage '${current}' has failing checks.`);
266
+ if (gate.filesMissing.length) {
267
+ console.log('Missing files:');
268
+ gate.filesMissing.forEach((f) => console.log(`- ${f}`));
269
+ }
270
+ if (gate.commandFailures.length) {
271
+ console.log('Failed commands:');
272
+ gate.commandFailures.forEach((c) => console.log(`- ${c}`));
273
+ }
274
+ return 1;
275
+ }
276
+
277
+ state.current_stage = targetStage;
278
+ state.last_updated = new Date().toISOString();
279
+ saveYaml(STATE_PATH, state);
280
+
281
+ history.transitions.push({
282
+ from: current,
283
+ to: targetStage,
284
+ at: state.last_updated,
285
+ actor: process.env.USER || process.env.USERNAME || 'unknown',
286
+ });
287
+ saveYaml(HISTORY_PATH, history);
288
+
289
+ console.log(`Promoted workflow: ${current} -> ${targetStage}`);
290
+ return 0;
291
+ }
292
+
293
+ function copyDirectory(src, dest) {
294
+ fs.mkdirSync(dest, { recursive: true });
295
+ const entries = fs.readdirSync(src, { withFileTypes: true });
296
+
297
+ for (const entry of entries) {
298
+ const srcPath = path.join(src, entry.name);
299
+ const destPath = path.join(dest, entry.name);
300
+
301
+ if (entry.isDirectory()) {
302
+ copyDirectory(srcPath, destPath);
303
+ } else if (!fileExists(destPath)) {
304
+ fs.copyFileSync(srcPath, destPath);
305
+ }
306
+ }
307
+ }
308
+
309
+ function init() {
310
+ const srcWorkflow = path.join(__dirname, '..', 'templates', 'workflow');
311
+ const destWorkflow = path.join(process.cwd(), 'workflow');
312
+
313
+ copyDirectory(srcWorkflow, destWorkflow);
314
+
315
+ let needsHistoryBootstrap = !fileExists(HISTORY_PATH);
316
+ if (!needsHistoryBootstrap) {
317
+ try {
318
+ const existingHistory = loadYaml(HISTORY_PATH);
319
+ needsHistoryBootstrap = !existingHistory || !Array.isArray(existingHistory.transitions) || existingHistory.transitions.length === 0;
320
+ } catch (error) {
321
+ needsHistoryBootstrap = true;
322
+ }
323
+ }
324
+
325
+ if (needsHistoryBootstrap) {
326
+ const currentState = loadYaml(STATE_PATH);
327
+ const initializedAt = new Date().toISOString();
328
+ saveYaml(HISTORY_PATH, {
329
+ transitions: [
330
+ {
331
+ from: null,
332
+ to: currentState.current_stage,
333
+ at: initializedAt,
334
+ actor: process.env.USER || process.env.USERNAME || 'unknown',
335
+ },
336
+ ],
337
+ });
338
+ currentState.last_updated = initializedAt;
339
+ saveYaml(STATE_PATH, currentState);
340
+ }
341
+
342
+ console.log('Initialized workflow files in ./workflow');
343
+ return 0;
344
+ }
345
+
346
+ function gh(args) {
347
+ const result = spawnSync('gh', args, {
348
+ stdio: 'inherit',
349
+ cwd: process.cwd(),
350
+ });
351
+ return result.status === null ? 1 : result.status;
352
+ }
353
+
354
+ function ghCapture(args) {
355
+ const result = spawnSync('gh', args, {
356
+ stdio: ['ignore', 'pipe', 'pipe'],
357
+ cwd: process.cwd(),
358
+ encoding: 'utf8',
359
+ });
360
+ if (result.status !== 0) {
361
+ return null;
362
+ }
363
+ return (result.stdout || '').trim();
364
+ }
365
+
366
+ function normalizeVisibilityFlag(input) {
367
+ if (input === '1') {
368
+ return '--private';
369
+ }
370
+ if (input === '2') {
371
+ return '--public';
372
+ }
373
+ return input;
374
+ }
375
+
376
+ function repo(owner, repoName, visibilityFlag = '--private', explicitTemplateRepo = '') {
377
+ if (!owner || !repoName) {
378
+ throw new Error('Usage: jarvis-control repo <owner> <repo-name> [--public|--private] [--template owner/repo]');
379
+ }
380
+ visibilityFlag = normalizeVisibilityFlag(visibilityFlag);
381
+ if (visibilityFlag !== '--private' && visibilityFlag !== '--public') {
382
+ throw new Error("Visibility must be '--private' or '--public'");
383
+ }
384
+
385
+ const ghVersion = ghCapture(['--version']);
386
+ if (!ghVersion) {
387
+ throw new Error('GitHub CLI (gh) is required. Install: https://cli.github.com/');
388
+ }
389
+
390
+ const templateRepo =
391
+ explicitTemplateRepo ||
392
+ ghCapture(['repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner']);
393
+ if (!templateRepo) {
394
+ throw new Error(
395
+ 'Unable to detect template repo. Run inside a template repo clone or pass --template <owner/repo> (or set JARVIS_TEMPLATE_REPO).',
396
+ );
397
+ }
398
+
399
+ console.log(`${BOLD}${CYAN}Creating repo from template${RESET}`);
400
+ console.log(`Template: ${templateRepo}`);
401
+ console.log(`Target: ${owner}/${repoName}`);
402
+ console.log(`Visibility: ${visibilityFlag.replace('--', '')}`);
403
+ console.log('');
404
+
405
+ const status = gh(['repo', 'create', `${owner}/${repoName}`, visibilityFlag, '--template', templateRepo, '--clone']);
406
+ if (status !== 0) {
407
+ return status;
408
+ }
409
+
410
+ console.log('');
411
+ console.log(`${GREEN}Repository created and cloned successfully.${RESET}`);
412
+ console.log(`Next: cd ${repoName}`);
413
+ return 0;
414
+ }
415
+
416
+ module.exports = {
417
+ init,
418
+ status,
419
+ check,
420
+ promote,
421
+ repo,
422
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@n12r/jarvis-control",
3
+ "version": "0.2.3",
4
+ "description": "Deterministic workflow gates for AI-assisted delivery",
5
+ "bin": {
6
+ "jarvis-control": "bin/jarvis.js"
7
+ },
8
+ "type": "commonjs",
9
+ "scripts": {
10
+ "cp:init": "node bin/jarvis.js init",
11
+ "cp:status": "node bin/jarvis.js status",
12
+ "cp:check": "node bin/jarvis.js check",
13
+ "cp:promote": "node bin/jarvis.js promote",
14
+ "cp:guard-state": "node scripts/guard-state-change.js",
15
+ "cp:apply-protections": "bash scripts/apply-github-protections.sh",
16
+ "cp:create-repo": "bash scripts/create-repo-from-template.sh"
17
+ },
18
+ "keywords": [
19
+ "control-plane",
20
+ "workflow",
21
+ "gates",
22
+ "ai"
23
+ ],
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "js-yaml": "^4.1.0"
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ # change-note
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # constraints
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # contracts
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # error-model
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # implementation-plan
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # observability
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # post-release-review
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # release-checklist
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # risks
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # rollback
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # scope
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # security-review
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,6 @@
1
+ # test-plan
2
+
3
+ Status: draft
4
+ Owner:
5
+ Last updated:
6
+
@@ -0,0 +1,57 @@
1
+ stages:
2
+ discovery:
3
+ allowed_next:
4
+ - contract
5
+ required_files:
6
+ - workflow/artifacts/scope.md
7
+ - workflow/artifacts/constraints.md
8
+ required_commands: []
9
+
10
+ contract:
11
+ allowed_next:
12
+ - plan
13
+ required_files:
14
+ - workflow/artifacts/contracts.md
15
+ - workflow/artifacts/error-model.md
16
+ required_commands: []
17
+
18
+ plan:
19
+ allowed_next:
20
+ - build
21
+ required_files:
22
+ - workflow/artifacts/implementation-plan.md
23
+ - workflow/artifacts/test-plan.md
24
+ - workflow/artifacts/risks.md
25
+ required_commands: []
26
+
27
+ build:
28
+ allowed_next:
29
+ - harden
30
+ required_files:
31
+ - workflow/artifacts/change-note.md
32
+ required_commands:
33
+ - npm test
34
+
35
+ harden:
36
+ allowed_next:
37
+ - release
38
+ required_files:
39
+ - workflow/artifacts/observability.md
40
+ - workflow/artifacts/security-review.md
41
+ required_commands:
42
+ - npm test
43
+
44
+ release:
45
+ allowed_next:
46
+ - operate
47
+ required_files:
48
+ - workflow/artifacts/rollback.md
49
+ - workflow/artifacts/release-checklist.md
50
+ required_commands:
51
+ - npm test
52
+
53
+ operate:
54
+ allowed_next: []
55
+ required_files:
56
+ - workflow/artifacts/post-release-review.md
57
+ required_commands: []
@@ -0,0 +1,3 @@
1
+ current_stage: discovery
2
+ owner: ""
3
+ last_updated: ""