agentxchain 2.11.0 → 2.12.0
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 +11 -5
- package/bin/agentxchain.js +3 -0
- package/package.json +1 -1
- package/src/commands/init.js +163 -30
- package/src/lib/gate-evaluator.js +13 -0
- package/src/lib/normalized-config.js +1 -1
- package/src/lib/workflow-gate-semantics.js +79 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npm install -g agentxchain
|
|
|
25
25
|
Or run without installing:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx agentxchain init --governed -y
|
|
28
|
+
npx agentxchain init --governed --dir my-agentxchain-project -y
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Testing
|
|
@@ -49,7 +49,7 @@ Duplicate execution remains intentional for the current 36-file slice until a la
|
|
|
49
49
|
### Governed workflow
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
npx agentxchain init --governed -y
|
|
52
|
+
npx agentxchain init --governed --dir my-agentxchain-project -y
|
|
53
53
|
cd my-agentxchain-project
|
|
54
54
|
git init
|
|
55
55
|
git add -A
|
|
@@ -58,10 +58,16 @@ agentxchain status
|
|
|
58
58
|
agentxchain step --role pm
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The default governed dev runtime is `claude --print` with stdin prompt delivery. If your local coding agent uses a different launch contract, set it during scaffold creation:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx agentxchain init --governed --dir my-agentxchain-project --dev-command ./scripts/dev-agent.sh --dev-prompt-transport dispatch_bundle_only -y
|
|
65
|
+
```
|
|
66
|
+
|
|
61
67
|
If you want template-specific planning artifacts from day one:
|
|
62
68
|
|
|
63
69
|
```bash
|
|
64
|
-
npx agentxchain init --governed --template api-service -y
|
|
70
|
+
npx agentxchain init --governed --template api-service --dir my-agentxchain-project -y
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
Built-in governed templates:
|
|
@@ -83,7 +89,7 @@ agentxchain step --role qa
|
|
|
83
89
|
agentxchain approve-completion
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
Default governed scaffolding configures QA as `api_proxy` with `ANTHROPIC_API_KEY`. For a provider-free walkthrough, switch the QA runtime to `manual` before the QA step.
|
|
92
|
+
Default governed scaffolding configures QA as `api_proxy` with `ANTHROPIC_API_KEY`. For a provider-free walkthrough, switch the QA runtime to `manual` before the QA step. If you override the dev runtime, either include `{prompt}` for argv delivery or set `--dev-prompt-transport` explicitly.
|
|
87
93
|
|
|
88
94
|
### Migrate a legacy project
|
|
89
95
|
|
|
@@ -99,7 +105,7 @@ agentxchain step
|
|
|
99
105
|
|
|
100
106
|
| Command | What it does |
|
|
101
107
|
|---|---|
|
|
102
|
-
| `init --governed [--template <id>]` | Create a governed project, optionally with project-shape-specific planning artifacts |
|
|
108
|
+
| `init --governed [--dir <path>] [--template <id>]` | Create a governed project, optionally in-place or in an explicit target directory, with project-shape-specific planning artifacts |
|
|
103
109
|
| `migrate` | Convert a legacy v3 project to governed format |
|
|
104
110
|
| `status` | Show current run, template, phase, turn, and approval state |
|
|
105
111
|
| `resume` | Initialize or continue a governed run and assign the next turn |
|
package/bin/agentxchain.js
CHANGED
|
@@ -115,7 +115,10 @@ program
|
|
|
115
115
|
.description('Create a new AgentXchain project folder')
|
|
116
116
|
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
117
117
|
.option('--governed', 'Create a governed project (orchestrator-owned state)')
|
|
118
|
+
.option('--dir <path>', 'Scaffold target directory. Use "." for in-place bootstrap.')
|
|
118
119
|
.option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app')
|
|
120
|
+
.option('--dev-command <parts...>', 'Governed local-dev command parts. Include {prompt} for argv prompt delivery.')
|
|
121
|
+
.option('--dev-prompt-transport <mode>', 'Governed local-dev prompt transport: argv, stdin, dispatch_bundle_only')
|
|
119
122
|
.option('--schema-version <version>', 'Schema version (3 for legacy, or use --governed for current)')
|
|
120
123
|
.action(initCommand);
|
|
121
124
|
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
2
|
-
import { join, resolve, dirname } from 'path';
|
|
2
|
+
import { basename, join, relative, resolve, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { CONFIG_FILE, LOCK_FILE, STATE_FILE } from '../lib/config.js';
|
|
7
7
|
import { generateVSCodeFiles } from '../lib/generate-vscode.js';
|
|
8
8
|
import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
|
|
9
|
+
import { VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@@ -93,9 +94,16 @@ const GOVERNED_ROLES = {
|
|
|
93
94
|
}
|
|
94
95
|
};
|
|
95
96
|
|
|
97
|
+
const DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME = Object.freeze({
|
|
98
|
+
type: 'local_cli',
|
|
99
|
+
command: ['claude', '--print'],
|
|
100
|
+
cwd: '.',
|
|
101
|
+
prompt_transport: 'stdin',
|
|
102
|
+
});
|
|
103
|
+
|
|
96
104
|
const GOVERNED_RUNTIMES = {
|
|
97
105
|
'manual-pm': { type: 'manual' },
|
|
98
|
-
'local-dev':
|
|
106
|
+
'local-dev': DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME,
|
|
99
107
|
'api-qa': { type: 'api_proxy', provider: 'anthropic', model: 'claude-sonnet-4-6', auth_env: 'ANTHROPIC_API_KEY' },
|
|
100
108
|
'manual-director': { type: 'manual' }
|
|
101
109
|
};
|
|
@@ -353,8 +361,92 @@ ${role.write_authority === 'authoritative'
|
|
|
353
361
|
`;
|
|
354
362
|
}
|
|
355
363
|
|
|
356
|
-
|
|
364
|
+
function commandHasPromptPlaceholder(parts = []) {
|
|
365
|
+
return parts.some((part) => typeof part === 'string' && part.includes('{prompt}'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveGovernedLocalDevRuntime(opts = {}) {
|
|
369
|
+
const customCommand = Array.isArray(opts.devCommand)
|
|
370
|
+
? opts.devCommand.map((part) => String(part).trim()).filter(Boolean)
|
|
371
|
+
: null;
|
|
372
|
+
const explicitTransport = typeof opts.devPromptTransport === 'string' && opts.devPromptTransport.trim()
|
|
373
|
+
? opts.devPromptTransport.trim()
|
|
374
|
+
: null;
|
|
375
|
+
|
|
376
|
+
if (explicitTransport && !VALID_PROMPT_TRANSPORTS.includes(explicitTransport)) {
|
|
377
|
+
throw new Error(`Unknown --dev-prompt-transport "${explicitTransport}". Valid values: ${VALID_PROMPT_TRANSPORTS.join(', ')}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!customCommand?.length) {
|
|
381
|
+
const command = [...DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME.command];
|
|
382
|
+
if (explicitTransport === 'argv') {
|
|
383
|
+
throw new Error('Default local dev command does not include {prompt}. Use --dev-command ... {prompt} for argv mode.');
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
runtime: {
|
|
387
|
+
...DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME,
|
|
388
|
+
command,
|
|
389
|
+
prompt_transport: explicitTransport || DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME.prompt_transport,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const hasPlaceholder = commandHasPromptPlaceholder(customCommand);
|
|
395
|
+
|
|
396
|
+
if (!explicitTransport && !hasPlaceholder) {
|
|
397
|
+
throw new Error('Custom --dev-command must either include {prompt} or set --dev-prompt-transport explicitly.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (explicitTransport === 'argv' && !hasPlaceholder) {
|
|
401
|
+
throw new Error('--dev-prompt-transport argv requires {prompt} in --dev-command.');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (explicitTransport && explicitTransport !== 'argv' && hasPlaceholder) {
|
|
405
|
+
throw new Error(`--dev-prompt-transport ${explicitTransport} must not be combined with {prompt} in --dev-command.`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
runtime: {
|
|
410
|
+
type: 'local_cli',
|
|
411
|
+
command: customCommand,
|
|
412
|
+
cwd: '.',
|
|
413
|
+
prompt_transport: explicitTransport || 'argv',
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function formatGovernedRuntimeCommand(runtime) {
|
|
419
|
+
return Array.isArray(runtime?.command) ? runtime.command.join(' ') : String(runtime?.command || '');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function resolveInitDirOption(dirOption) {
|
|
423
|
+
if (dirOption == null) return null;
|
|
424
|
+
const value = String(dirOption).trim();
|
|
425
|
+
if (!value) {
|
|
426
|
+
throw new Error('--dir must not be empty.');
|
|
427
|
+
}
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function inferProjectNameFromTarget(targetPath, fallbackName) {
|
|
432
|
+
const inferred = basename(resolve(process.cwd(), targetPath));
|
|
433
|
+
return inferred && inferred.trim() ? inferred : fallbackName;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function formatInitTarget(dir) {
|
|
437
|
+
const rel = relative(process.cwd(), dir);
|
|
438
|
+
if (!rel) return '.';
|
|
439
|
+
if (!rel.startsWith('..')) return rel;
|
|
440
|
+
return dir;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}) {
|
|
357
444
|
const template = loadGovernedTemplate(templateId);
|
|
445
|
+
const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
|
|
446
|
+
const runtimes = {
|
|
447
|
+
...GOVERNED_RUNTIMES,
|
|
448
|
+
'local-dev': localDevRuntime,
|
|
449
|
+
};
|
|
358
450
|
const config = {
|
|
359
451
|
schema_version: '1.0',
|
|
360
452
|
template: template.id,
|
|
@@ -364,7 +456,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
364
456
|
default_branch: 'main'
|
|
365
457
|
},
|
|
366
458
|
roles: GOVERNED_ROLES,
|
|
367
|
-
runtimes
|
|
459
|
+
runtimes,
|
|
368
460
|
routing: GOVERNED_ROUTING,
|
|
369
461
|
gates: GOVERNED_GATES,
|
|
370
462
|
budget: {
|
|
@@ -475,6 +567,14 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
475
567
|
async function initGoverned(opts) {
|
|
476
568
|
let projectName, folderName;
|
|
477
569
|
const templateId = opts.template || 'generic';
|
|
570
|
+
let explicitDir;
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
explicitDir = resolveInitDirOption(opts.dir);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
478
578
|
|
|
479
579
|
if (!VALID_GOVERNED_TEMPLATE_IDS.includes(templateId)) {
|
|
480
580
|
console.error(chalk.red(` Error: Unknown template "${templateId}".`));
|
|
@@ -489,29 +589,44 @@ async function initGoverned(opts) {
|
|
|
489
589
|
}
|
|
490
590
|
|
|
491
591
|
if (opts.yes) {
|
|
492
|
-
projectName =
|
|
493
|
-
|
|
592
|
+
projectName = explicitDir
|
|
593
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
|
|
594
|
+
: 'My AgentXchain Project';
|
|
595
|
+
folderName = explicitDir || slugify(projectName);
|
|
494
596
|
} else {
|
|
495
597
|
const { name } = await inquirer.prompt([{
|
|
496
598
|
type: 'input',
|
|
497
599
|
name: 'name',
|
|
498
600
|
message: 'Project name:',
|
|
499
|
-
default:
|
|
601
|
+
default: explicitDir
|
|
602
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
|
|
603
|
+
: 'My AgentXchain Project'
|
|
500
604
|
}]);
|
|
501
605
|
projectName = name;
|
|
502
|
-
folderName = slugify(projectName);
|
|
606
|
+
folderName = explicitDir || slugify(projectName);
|
|
503
607
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
608
|
+
if (!explicitDir) {
|
|
609
|
+
const { folder } = await inquirer.prompt([{
|
|
610
|
+
type: 'input',
|
|
611
|
+
name: 'folder',
|
|
612
|
+
message: 'Folder name:',
|
|
613
|
+
default: folderName
|
|
614
|
+
}]);
|
|
615
|
+
folderName = folder;
|
|
616
|
+
}
|
|
511
617
|
}
|
|
512
618
|
|
|
513
619
|
const dir = resolve(process.cwd(), folderName);
|
|
620
|
+
const targetLabel = formatInitTarget(dir);
|
|
514
621
|
const projectId = slugify(projectName);
|
|
622
|
+
let localDevRuntime;
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
({ runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(opts));
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
515
630
|
|
|
516
631
|
if (existsSync(dir) && existsSync(join(dir, CONFIG_FILE))) {
|
|
517
632
|
if (!opts.yes) {
|
|
@@ -530,10 +645,10 @@ async function initGoverned(opts) {
|
|
|
530
645
|
|
|
531
646
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
532
647
|
|
|
533
|
-
scaffoldGoverned(dir, projectName, projectId, templateId);
|
|
648
|
+
scaffoldGoverned(dir, projectName, projectId, templateId, opts);
|
|
534
649
|
|
|
535
650
|
console.log('');
|
|
536
|
-
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(
|
|
651
|
+
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
537
652
|
console.log('');
|
|
538
653
|
console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim('(governed)')}`);
|
|
539
654
|
console.log(` ${chalk.dim('├──')} .agentxchain/`);
|
|
@@ -549,10 +664,13 @@ async function initGoverned(opts) {
|
|
|
549
664
|
console.log('');
|
|
550
665
|
console.log(` ${chalk.dim('Roles:')} pm, dev, qa, eng_director`);
|
|
551
666
|
console.log(` ${chalk.dim('Template:')} ${templateId}`);
|
|
667
|
+
console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
|
|
552
668
|
console.log(` ${chalk.dim('Protocol:')} governed convergence`);
|
|
553
669
|
console.log('');
|
|
554
670
|
console.log(` ${chalk.cyan('Next:')}`);
|
|
555
|
-
|
|
671
|
+
if (dir !== process.cwd()) {
|
|
672
|
+
console.log(` ${chalk.bold(`cd ${targetLabel}`)}`);
|
|
673
|
+
}
|
|
556
674
|
console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);
|
|
557
675
|
console.log(` ${chalk.bold('agentxchain status')} ${chalk.dim('# inspect phase, gate, and turn state')}`);
|
|
558
676
|
console.log('');
|
|
@@ -564,11 +682,20 @@ export async function initCommand(opts) {
|
|
|
564
682
|
}
|
|
565
683
|
|
|
566
684
|
let project, agents, folderName, rules;
|
|
685
|
+
let explicitDir;
|
|
686
|
+
try {
|
|
687
|
+
explicitDir = resolveInitDirOption(opts.dir);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
567
692
|
|
|
568
693
|
if (opts.yes) {
|
|
569
|
-
project =
|
|
694
|
+
project = explicitDir
|
|
695
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain project')
|
|
696
|
+
: 'My AgentXchain project';
|
|
570
697
|
agents = DEFAULT_AGENTS;
|
|
571
|
-
folderName = slugify(project);
|
|
698
|
+
folderName = explicitDir || slugify(project);
|
|
572
699
|
rules = {
|
|
573
700
|
max_consecutive_claims: 2,
|
|
574
701
|
require_message: true,
|
|
@@ -618,7 +745,9 @@ export async function initCommand(opts) {
|
|
|
618
745
|
type: 'input',
|
|
619
746
|
name: 'projectName',
|
|
620
747
|
message: 'Project name:',
|
|
621
|
-
default:
|
|
748
|
+
default: explicitDir
|
|
749
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain project')
|
|
750
|
+
: 'My AgentXchain project'
|
|
622
751
|
}]);
|
|
623
752
|
project = projectName;
|
|
624
753
|
} else {
|
|
@@ -626,7 +755,9 @@ export async function initCommand(opts) {
|
|
|
626
755
|
type: 'input',
|
|
627
756
|
name: 'projectName',
|
|
628
757
|
message: 'Project name:',
|
|
629
|
-
default:
|
|
758
|
+
default: explicitDir
|
|
759
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain project')
|
|
760
|
+
: 'My AgentXchain project'
|
|
630
761
|
}]);
|
|
631
762
|
project = projectName;
|
|
632
763
|
agents = {};
|
|
@@ -681,14 +812,16 @@ export async function initCommand(opts) {
|
|
|
681
812
|
}
|
|
682
813
|
}
|
|
683
814
|
|
|
684
|
-
folderName = slugify(project);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
815
|
+
folderName = explicitDir || slugify(project);
|
|
816
|
+
if (!explicitDir) {
|
|
817
|
+
const { folder } = await inquirer.prompt([{
|
|
818
|
+
type: 'input',
|
|
819
|
+
name: 'folder',
|
|
820
|
+
message: 'Folder name:',
|
|
821
|
+
default: folderName
|
|
822
|
+
}]);
|
|
823
|
+
folderName = folder;
|
|
824
|
+
}
|
|
692
825
|
}
|
|
693
826
|
|
|
694
827
|
const dir = resolve(process.cwd(), folderName);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync } from 'fs';
|
|
21
21
|
import { join } from 'path';
|
|
22
|
+
import { evaluateWorkflowGateSemantics } from './workflow-gate-semantics.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Evaluate whether the current phase exit gate is satisfied.
|
|
@@ -113,6 +114,12 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
|
|
|
113
114
|
if (!existsSync(join(root, filePath))) {
|
|
114
115
|
result.missing_files.push(filePath);
|
|
115
116
|
failures.push(`Required file missing: ${filePath}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const semanticCheck = evaluateWorkflowGateSemantics(root, filePath);
|
|
121
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
122
|
+
failures.push(semanticCheck.reason);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
}
|
|
@@ -229,6 +236,12 @@ export function evaluateRunCompletion({ state, config, acceptedTurn, root }) {
|
|
|
229
236
|
if (!existsSync(join(root, filePath))) {
|
|
230
237
|
result.missing_files.push(filePath);
|
|
231
238
|
failures.push(`Required file missing: ${filePath}`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const semanticCheck = evaluateWorkflowGateSemantics(root, filePath);
|
|
243
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
244
|
+
failures.push(semanticCheck.reason);
|
|
232
245
|
}
|
|
233
246
|
}
|
|
234
247
|
}
|
|
@@ -19,7 +19,7 @@ import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
|
19
19
|
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
20
20
|
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp'];
|
|
21
21
|
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
22
|
-
const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
22
|
+
export const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
23
23
|
const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
|
|
24
24
|
const VALID_PHASES = ['planning', 'implementation', 'qa'];
|
|
25
25
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const PM_SIGNOFF_PATH = '.planning/PM_SIGNOFF.md';
|
|
5
|
+
export const SHIP_VERDICT_PATH = '.planning/ship-verdict.md';
|
|
6
|
+
|
|
7
|
+
const AFFIRMATIVE_SHIP_VERDICTS = new Set(['YES', 'SHIP', 'SHIP IT']);
|
|
8
|
+
|
|
9
|
+
function normalizeToken(value) {
|
|
10
|
+
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readFile(root, relPath) {
|
|
14
|
+
const absPath = join(root, relPath);
|
|
15
|
+
if (!existsSync(absPath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return readFileSync(absPath, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseLineValue(content, pattern) {
|
|
22
|
+
const match = content.match(pattern);
|
|
23
|
+
return match ? match[1].trim() : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function evaluatePmSignoff(content) {
|
|
27
|
+
const approved = parseLineValue(content, /^Approved\s*:\s*(.+)$/im);
|
|
28
|
+
if (!approved) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: 'PM signoff must declare `Approved: YES` in .planning/PM_SIGNOFF.md.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (normalizeToken(approved) !== 'YES') {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: `PM signoff is not approved. Found "Approved: ${approved}" in .planning/PM_SIGNOFF.md; set it to "Approved: YES".`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function evaluateShipVerdict(content) {
|
|
46
|
+
const verdict = parseLineValue(content, /^##\s+Verdict\s*:\s*(.+)$/im);
|
|
47
|
+
if (!verdict) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: 'Ship verdict must declare an affirmative `## Verdict:` line in .planning/ship-verdict.md.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!AFFIRMATIVE_SHIP_VERDICTS.has(normalizeToken(verdict))) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: `Ship verdict is not affirmative. Found "## Verdict: ${verdict}" in .planning/ship-verdict.md; use "## Verdict: YES".`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { ok: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function evaluateWorkflowGateSemantics(root, relPath) {
|
|
65
|
+
const content = readFile(root, relPath);
|
|
66
|
+
if (content === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (relPath === PM_SIGNOFF_PATH) {
|
|
71
|
+
return evaluatePmSignoff(content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (relPath === SHIP_VERDICT_PATH) {
|
|
75
|
+
return evaluateShipVerdict(content);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|