agentxchain 2.102.0 → 2.104.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 +1 -1
- package/bin/agentxchain.js +11 -3
- package/package.json +1 -1
- package/scripts/release-preflight.sh +82 -38
- package/src/commands/decisions.js +29 -3
- package/src/commands/generate.js +126 -1
- package/src/commands/init.js +15 -97
- package/src/commands/role.js +24 -10
- package/src/lib/dispatch-bundle.js +1 -1
- package/src/lib/export-verifier.js +4 -23
- package/src/lib/export.js +4 -14
- package/src/lib/governed-state.js +3 -1
- package/src/lib/normalized-config.js +5 -0
- package/src/lib/planning-artifacts.js +131 -0
- package/src/lib/repo-decisions.js +163 -3
- package/src/lib/report.js +40 -5
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ agentxchain step
|
|
|
224
224
|
| `supervise` | Run `watch` plus optional macOS auto-nudge |
|
|
225
225
|
| `claim` / `release` | Human override of legacy lock ownership |
|
|
226
226
|
| `rebind` | Rebuild Cursor bindings |
|
|
227
|
-
| `generate` | Regenerate VS Code agent files |
|
|
227
|
+
| `generate` | Regenerate VS Code agent files; use `generate planning` to restore scaffold-owned governed planning docs |
|
|
228
228
|
| `branch` | Manage Cursor branch override for launches |
|
|
229
229
|
| `doctor` | Check local environment and setup |
|
|
230
230
|
| `stop` | Stop watch daemon and local sessions |
|
package/bin/agentxchain.js
CHANGED
|
@@ -55,7 +55,7 @@ import { configCommand } from '../src/commands/config.js';
|
|
|
55
55
|
import { updateCommand } from '../src/commands/update.js';
|
|
56
56
|
import { watchCommand } from '../src/commands/watch.js';
|
|
57
57
|
import { claimCommand, releaseCommand } from '../src/commands/claim.js';
|
|
58
|
-
import { generateCommand } from '../src/commands/generate.js';
|
|
58
|
+
import { generateCommand, generatePlanningCommand } from '../src/commands/generate.js';
|
|
59
59
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
@@ -223,11 +223,19 @@ program
|
|
|
223
223
|
.option('--unset', 'Remove override and follow the active git branch automatically')
|
|
224
224
|
.action(branchCommand);
|
|
225
225
|
|
|
226
|
-
program
|
|
226
|
+
const generateCmd = program
|
|
227
227
|
.command('generate')
|
|
228
|
-
.description('Regenerate VS Code agent files
|
|
228
|
+
.description('Regenerate VS Code agent files, or governed planning artifacts via subcommands')
|
|
229
229
|
.action(generateCommand);
|
|
230
230
|
|
|
231
|
+
generateCmd
|
|
232
|
+
.command('planning')
|
|
233
|
+
.description('Generate or restore scaffold-owned governed planning artifacts')
|
|
234
|
+
.option('--dry-run', 'Show which planning artifacts would be written without changing files')
|
|
235
|
+
.option('--force', 'Overwrite existing scaffold-owned planning artifacts')
|
|
236
|
+
.option('-j, --json', 'Output as JSON')
|
|
237
|
+
.action(generatePlanningCommand);
|
|
238
|
+
|
|
231
239
|
program
|
|
232
240
|
.command('watch')
|
|
233
241
|
.description('Watch lock.json and coordinate agent turns (the referee)')
|
package/package.json
CHANGED
|
@@ -9,10 +9,12 @@ CLI_DIR="${SCRIPT_DIR}/.."
|
|
|
9
9
|
cd "$CLI_DIR"
|
|
10
10
|
|
|
11
11
|
STRICT_MODE=0
|
|
12
|
+
PUBLISH_GATE=0
|
|
12
13
|
TARGET_VERSION="2.0.0"
|
|
13
14
|
|
|
14
15
|
usage() {
|
|
15
|
-
echo "Usage: bash scripts/release-preflight.sh [--strict] [--target-version <semver>]" >&2
|
|
16
|
+
echo "Usage: bash scripts/release-preflight.sh [--strict] [--publish-gate] [--target-version <semver>]" >&2
|
|
17
|
+
echo " --publish-gate Run only release-critical checks (no full test suite). Use in CI publish workflows." >&2
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
while [[ $# -gt 0 ]]; do
|
|
@@ -21,6 +23,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
21
23
|
STRICT_MODE=1
|
|
22
24
|
shift
|
|
23
25
|
;;
|
|
26
|
+
--publish-gate)
|
|
27
|
+
PUBLISH_GATE=1
|
|
28
|
+
STRICT_MODE=1
|
|
29
|
+
shift
|
|
30
|
+
;;
|
|
24
31
|
--target-version)
|
|
25
32
|
if [[ -z "${2:-}" ]]; then
|
|
26
33
|
echo "Error: --target-version requires a semver argument" >&2
|
|
@@ -99,49 +106,86 @@ else
|
|
|
99
106
|
fi
|
|
100
107
|
|
|
101
108
|
# 3. Tests
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
if [[ "$PUBLISH_GATE" -eq 1 ]]; then
|
|
110
|
+
echo "[3/6] Release-gate tests (targeted subset)"
|
|
111
|
+
# In publish-gate mode, run only release-critical tests to avoid CI hangs.
|
|
112
|
+
# The full test suite is a pre-tag responsibility, not a publish-time gate.
|
|
113
|
+
GATE_TESTS=(
|
|
114
|
+
test/release-preflight.test.js
|
|
115
|
+
test/release-docs-content.test.js
|
|
116
|
+
test/release-notes-gate.test.js
|
|
117
|
+
test/release-identity-hardening.test.js
|
|
118
|
+
test/normalized-config.test.js
|
|
119
|
+
test/conformance.test.js
|
|
120
|
+
)
|
|
121
|
+
GATE_TEST_ARGS=()
|
|
122
|
+
for t in "${GATE_TESTS[@]}"; do
|
|
123
|
+
if [[ -f "$t" ]]; then
|
|
124
|
+
GATE_TEST_ARGS+=("$t")
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
if [[ ${#GATE_TEST_ARGS[@]} -eq 0 ]]; then
|
|
128
|
+
fail "No release-gate test files found"
|
|
129
|
+
else
|
|
130
|
+
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
|
|
131
|
+
TEST_STATUS=0
|
|
132
|
+
else
|
|
133
|
+
TEST_STATUS=$?
|
|
134
|
+
fi
|
|
135
|
+
NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
|
|
136
|
+
NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
|
|
137
|
+
if [ "$TEST_STATUS" -eq 0 ] && [ "${NODE_FAIL:-0}" = "0" ]; then
|
|
138
|
+
pass "${NODE_PASS:-?} release-gate tests passed, 0 failures"
|
|
139
|
+
else
|
|
140
|
+
fail "Release-gate tests failed"
|
|
141
|
+
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
142
|
+
fi
|
|
108
143
|
fi
|
|
109
|
-
done
|
|
110
|
-
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
|
|
111
|
-
TEST_STATUS=0
|
|
112
144
|
else
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
TEST_PASS="${VITEST_PASS}"
|
|
145
|
+
echo "[3/6] Test suite"
|
|
146
|
+
# Install MCP example deps — tests start example servers as subprocesses
|
|
147
|
+
for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
|
|
148
|
+
if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
|
|
149
|
+
echo " Installing deps for $(basename "$example_dir")..."
|
|
150
|
+
(cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
|
|
151
|
+
fi
|
|
152
|
+
done
|
|
153
|
+
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
|
|
154
|
+
TEST_STATUS=0
|
|
155
|
+
else
|
|
156
|
+
TEST_STATUS=$?
|
|
126
157
|
fi
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
|
|
159
|
+
TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
|
|
160
|
+
if [ -z "${TEST_PASS:-}" ]; then
|
|
161
|
+
VITEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
|
|
162
|
+
NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
|
|
163
|
+
if [ -n "${VITEST_PASS:-}" ] && [ -n "${NODE_PASS:-}" ]; then
|
|
164
|
+
TEST_PASS="$((VITEST_PASS + NODE_PASS))"
|
|
165
|
+
elif [ -n "${NODE_PASS:-}" ]; then
|
|
166
|
+
TEST_PASS="${NODE_PASS}"
|
|
167
|
+
elif [ -n "${VITEST_PASS:-}" ]; then
|
|
168
|
+
TEST_PASS="${VITEST_PASS}"
|
|
169
|
+
fi
|
|
134
170
|
fi
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
if [ -z "${TEST_FAIL:-}" ]; then
|
|
172
|
+
NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
|
|
173
|
+
if [ -n "${NODE_FAIL:-}" ]; then
|
|
174
|
+
TEST_FAIL="${NODE_FAIL}"
|
|
175
|
+
elif printf '%s\n' "$TEST_OUTPUT" | grep -Eq '^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed'; then
|
|
176
|
+
TEST_FAIL=0
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
|
|
180
|
+
if [ -n "${TEST_PASS:-}" ]; then
|
|
181
|
+
pass "${TEST_PASS} tests passed, 0 failures"
|
|
182
|
+
else
|
|
183
|
+
pass "npm test passed, 0 failures"
|
|
184
|
+
fi
|
|
139
185
|
else
|
|
140
|
-
|
|
186
|
+
fail "npm test failed"
|
|
187
|
+
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
141
188
|
fi
|
|
142
|
-
else
|
|
143
|
-
fail "npm test failed"
|
|
144
|
-
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
145
189
|
fi
|
|
146
190
|
|
|
147
191
|
# 4. CHANGELOG has target version
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById } from '../lib/repo-decisions.js';
|
|
10
|
+
import { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById, resolveDecisionAuthority } from '../lib/repo-decisions.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {object} opts - { json?: boolean, all?: boolean, show?: string, dir?: string }
|
|
@@ -39,6 +39,18 @@ export async function decisionsCommand(opts) {
|
|
|
39
39
|
console.log(` Phase: ${dec.phase || '—'}`);
|
|
40
40
|
console.log(` Run: ${(dec.run_id || '—').slice(0, 16)}`);
|
|
41
41
|
console.log(` Turn: ${(dec.turn_id || '—').slice(0, 16)}`);
|
|
42
|
+
console.log(` Durability: ${dec.durability || 'repo'}`);
|
|
43
|
+
// Show decision authority if config has it
|
|
44
|
+
const config = loadConfig(root);
|
|
45
|
+
if (config && dec.role) {
|
|
46
|
+
const auth = resolveDecisionAuthority(dec.role, config);
|
|
47
|
+
if (auth !== null && !(typeof auth === 'object' && auth.unknown)) {
|
|
48
|
+
console.log(` Authority: ${auth} (${dec.role})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (dec.overrides) {
|
|
52
|
+
console.log(` Supersedes: ${chalk.yellow(dec.overrides)}`);
|
|
53
|
+
}
|
|
42
54
|
console.log(` Created: ${dec.created_at || '—'}`);
|
|
43
55
|
if (dec.overridden_by) {
|
|
44
56
|
console.log(` Overridden: ${chalk.yellow(dec.overridden_by)}`);
|
|
@@ -69,7 +81,11 @@ export async function decisionsCommand(opts) {
|
|
|
69
81
|
for (const dec of decisions) {
|
|
70
82
|
const status = formatStatus(dec.status);
|
|
71
83
|
const runShort = (dec.run_id || '').slice(0, 12);
|
|
72
|
-
const override = dec.overridden_by
|
|
84
|
+
const override = dec.overridden_by
|
|
85
|
+
? chalk.dim(` → ${dec.overridden_by}`)
|
|
86
|
+
: dec.overrides
|
|
87
|
+
? chalk.dim(` ← supersedes ${dec.overrides}`)
|
|
88
|
+
: '';
|
|
73
89
|
console.log(` ${chalk.cyan(dec.id)} ${status} ${chalk.dim(dec.category)} ${dec.statement}${override}`);
|
|
74
90
|
console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}`)}`);
|
|
75
91
|
}
|
|
@@ -92,3 +108,13 @@ function findProjectRoot(dir) {
|
|
|
92
108
|
}
|
|
93
109
|
return null;
|
|
94
110
|
}
|
|
111
|
+
|
|
112
|
+
function loadConfig(root) {
|
|
113
|
+
const configPath = resolve(root, 'agentxchain.json');
|
|
114
|
+
if (!existsSync(configPath)) return null;
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/commands/generate.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
1
3
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig } from '../lib/config.js';
|
|
4
|
+
import { loadConfig, loadProjectContext } from '../lib/config.js';
|
|
3
5
|
import { generateVSCodeFiles } from '../lib/generate-vscode.js';
|
|
6
|
+
import { loadGovernedTemplate } from '../lib/governed-templates.js';
|
|
7
|
+
import { buildGovernedPlanningArtifacts } from '../lib/planning-artifacts.js';
|
|
4
8
|
|
|
5
9
|
export async function generateCommand() {
|
|
6
10
|
const result = loadConfig();
|
|
@@ -42,3 +46,124 @@ export async function generateCommand() {
|
|
|
42
46
|
console.log(chalk.dim(' Select an agent from the Chat dropdown to start a turn.'));
|
|
43
47
|
console.log('');
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
function failPlanningGenerate(message, opts = {}) {
|
|
51
|
+
if (opts.json) {
|
|
52
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(chalk.red(` ${message}`));
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function generatePlanningCommand(opts = {}) {
|
|
60
|
+
const context = loadProjectContext();
|
|
61
|
+
if (!context) {
|
|
62
|
+
failPlanningGenerate('No valid agentxchain.json found. Run `agentxchain init --governed` first.', opts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (context.version !== 4) {
|
|
66
|
+
failPlanningGenerate('`generate planning` only works in governed repos.', opts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const templateId = context.rawConfig.template || 'generic';
|
|
70
|
+
let template;
|
|
71
|
+
try {
|
|
72
|
+
template = loadGovernedTemplate(templateId);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
failPlanningGenerate(err.message, opts);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const projectName = context.config?.project?.name || context.rawConfig?.project?.name || 'AgentXchain Project';
|
|
78
|
+
const artifacts = buildGovernedPlanningArtifacts({
|
|
79
|
+
projectName,
|
|
80
|
+
routing: context.config.routing || {},
|
|
81
|
+
roles: context.config.roles || {},
|
|
82
|
+
template,
|
|
83
|
+
workflowKitConfig: context.config.workflow_kit || null,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const created = [];
|
|
87
|
+
const overwritten = [];
|
|
88
|
+
const skippedExisting = [];
|
|
89
|
+
|
|
90
|
+
for (const artifact of artifacts) {
|
|
91
|
+
const absPath = join(context.root, artifact.path);
|
|
92
|
+
if (existsSync(absPath)) {
|
|
93
|
+
if (opts.force) {
|
|
94
|
+
overwritten.push(artifact.path);
|
|
95
|
+
} else {
|
|
96
|
+
skippedExisting.push(artifact.path);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
created.push(artifact.path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (opts.dryRun) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parentDir = dirname(absPath);
|
|
108
|
+
if (!existsSync(parentDir)) {
|
|
109
|
+
mkdirSync(parentDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
writeFileSync(absPath, artifact.content);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const payload = {
|
|
115
|
+
ok: true,
|
|
116
|
+
mode: 'planning',
|
|
117
|
+
dry_run: Boolean(opts.dryRun),
|
|
118
|
+
force: Boolean(opts.force),
|
|
119
|
+
template: template.id,
|
|
120
|
+
project: projectName,
|
|
121
|
+
total_artifacts: artifacts.length,
|
|
122
|
+
created,
|
|
123
|
+
overwritten,
|
|
124
|
+
skipped_existing: skippedExisting,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (opts.json) {
|
|
128
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(chalk.bold(' Generating governed planning artifacts...'));
|
|
134
|
+
console.log(chalk.dim(` Project: ${projectName}`));
|
|
135
|
+
console.log(chalk.dim(` Template: ${template.id}`));
|
|
136
|
+
console.log('');
|
|
137
|
+
|
|
138
|
+
if (created.length > 0) {
|
|
139
|
+
console.log(chalk.green(` ${opts.dryRun ? 'Would create' : 'Created'} ${created.length} artifact${created.length === 1 ? '' : 's'}:`));
|
|
140
|
+
for (const path of created) {
|
|
141
|
+
console.log(chalk.green(` ${path}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (overwritten.length > 0) {
|
|
146
|
+
console.log(chalk.yellow(` ${opts.dryRun ? 'Would overwrite' : 'Overwrote'} ${overwritten.length} artifact${overwritten.length === 1 ? '' : 's'}:`));
|
|
147
|
+
for (const path of overwritten) {
|
|
148
|
+
console.log(chalk.yellow(` ${path}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (skippedExisting.length > 0) {
|
|
153
|
+
console.log(chalk.dim(` Preserved ${skippedExisting.length} existing artifact${skippedExisting.length === 1 ? '' : 's'}:`));
|
|
154
|
+
for (const path of skippedExisting) {
|
|
155
|
+
console.log(chalk.dim(` ${path}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (created.length === 0 && overwritten.length === 0) {
|
|
160
|
+
console.log(chalk.dim(` ${opts.force ? 'Nothing to overwrite.' : 'All scaffold-owned planning artifacts already exist.'}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (opts.dryRun) {
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.dim(' No files were written. Re-run without `--dry-run` to apply.'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -5,8 +5,9 @@ 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
|
-
import { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS
|
|
8
|
+
import { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
|
|
9
9
|
import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
10
|
+
import { buildGovernedPlanningArtifacts, interpolateTemplateContent } from '../lib/planning-artifacts.js';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@@ -47,24 +48,11 @@ function loadTemplates() {
|
|
|
47
48
|
return templates;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function interpolateTemplateContent(contentTemplate, projectName) {
|
|
51
|
-
return contentTemplate.replaceAll('{{project_name}}', projectName);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
51
|
function appendPromptOverride(basePrompt, override) {
|
|
55
52
|
if (!override || !override.trim()) return basePrompt;
|
|
56
53
|
return `${basePrompt}\n\n---\n\n## Project-Type-Specific Guidance\n\n${override.trim()}\n`;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
function appendAcceptanceHints(baseMatrix, acceptanceHints) {
|
|
60
|
-
if (!Array.isArray(acceptanceHints) || acceptanceHints.length === 0) {
|
|
61
|
-
return baseMatrix;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const hintLines = acceptanceHints.map((hint) => `- [ ] ${hint}`).join('\n');
|
|
65
|
-
return `${baseMatrix}\n\n## Template Guidance\n${hintLines}\n`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
56
|
function findGitRoot(startDir) {
|
|
69
57
|
let current = resolve(startDir);
|
|
70
58
|
while (true) {
|
|
@@ -598,20 +586,6 @@ export async function resolveGovernedInitAnswers(opts, prompt = (questions) => i
|
|
|
598
586
|
};
|
|
599
587
|
}
|
|
600
588
|
|
|
601
|
-
function generateWorkflowKitPlaceholder(artifact, projectName) {
|
|
602
|
-
const filename = basename(artifact.path);
|
|
603
|
-
const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
604
|
-
|
|
605
|
-
if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
|
|
606
|
-
const sections = artifact.semantics_config.required_sections
|
|
607
|
-
.map(s => `${s}\n\n(Content here.)\n`)
|
|
608
|
-
.join('\n');
|
|
609
|
-
return `# ${title} — ${projectName}\n\n${sections}`;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
589
|
function cloneJsonCompatible(value) {
|
|
616
590
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
617
591
|
}
|
|
@@ -653,29 +627,6 @@ function buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitC
|
|
|
653
627
|
};
|
|
654
628
|
}
|
|
655
629
|
|
|
656
|
-
const PHASE_DISPLAY_NAMES = Object.freeze({
|
|
657
|
-
qa: 'QA',
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
function formatPhaseDisplayName(phaseKey) {
|
|
661
|
-
if (PHASE_DISPLAY_NAMES[phaseKey]) {
|
|
662
|
-
return PHASE_DISPLAY_NAMES[phaseKey];
|
|
663
|
-
}
|
|
664
|
-
return phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function buildRoadmapPhaseTable(routing, roles) {
|
|
668
|
-
const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
|
|
669
|
-
const phaseName = formatPhaseDisplayName(phaseKey);
|
|
670
|
-
const entryRole = phaseConfig.entry_role;
|
|
671
|
-
const role = roles[entryRole];
|
|
672
|
-
const goal = role?.mandate || phaseName;
|
|
673
|
-
const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
|
|
674
|
-
return `| ${phaseName} | ${goal} | ${status} |`;
|
|
675
|
-
});
|
|
676
|
-
return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
630
|
function buildPlanningSummaryLines(template, workflowKitConfig) {
|
|
680
631
|
const lines = [
|
|
681
632
|
'PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md',
|
|
@@ -821,53 +772,20 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
821
772
|
}
|
|
822
773
|
|
|
823
774
|
// Planning artifacts
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
writeFileSync(join(dir, '.planning', 'RELEASE_NOTES.md'), `# Release Notes — ${projectName}\n\n## User Impact\n\n(QA fills this during the QA phase)\n\n## Verification Summary\n\n(QA fills this during the QA phase)\n\n## Upgrade Notes\n\n(QA fills this during the QA phase)\n\n## Known Issues\n\n(QA fills this during the QA phase)\n`);
|
|
835
|
-
for (const artifact of template.planning_artifacts) {
|
|
836
|
-
writeFileSync(
|
|
837
|
-
join(dir, '.planning', artifact.filename),
|
|
838
|
-
interpolateTemplateContent(artifact.content_template, projectName)
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
|
|
843
|
-
// that are not already handled by the default scaffold above
|
|
844
|
-
if (scaffoldWorkflowKitConfig && scaffoldWorkflowKitConfig.phases && typeof scaffoldWorkflowKitConfig.phases === 'object') {
|
|
845
|
-
const defaultScaffoldPaths = new Set([
|
|
846
|
-
'.planning/PM_SIGNOFF.md',
|
|
847
|
-
'.planning/ROADMAP.md',
|
|
848
|
-
'.planning/SYSTEM_SPEC.md',
|
|
849
|
-
'.planning/IMPLEMENTATION_NOTES.md',
|
|
850
|
-
'.planning/acceptance-matrix.md',
|
|
851
|
-
'.planning/ship-verdict.md',
|
|
852
|
-
'.planning/RELEASE_NOTES.md',
|
|
853
|
-
]);
|
|
854
|
-
|
|
855
|
-
for (const phaseConfig of Object.values(scaffoldWorkflowKitConfig.phases)) {
|
|
856
|
-
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
857
|
-
for (const artifact of phaseConfig.artifacts) {
|
|
858
|
-
if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
|
|
859
|
-
const absPath = join(dir, artifact.path);
|
|
860
|
-
if (existsSync(absPath)) continue;
|
|
861
|
-
|
|
862
|
-
// Ensure parent directory exists
|
|
863
|
-
const parentDir = dirname(absPath);
|
|
864
|
-
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
865
|
-
|
|
866
|
-
// Generate placeholder content based on semantics type
|
|
867
|
-
const content = generateWorkflowKitPlaceholder(artifact, projectName);
|
|
868
|
-
writeFileSync(absPath, content);
|
|
869
|
-
}
|
|
775
|
+
for (const artifact of buildGovernedPlanningArtifacts({
|
|
776
|
+
projectName,
|
|
777
|
+
routing,
|
|
778
|
+
roles,
|
|
779
|
+
template,
|
|
780
|
+
workflowKitConfig: scaffoldWorkflowKitConfig,
|
|
781
|
+
})) {
|
|
782
|
+
const absPath = join(dir, artifact.path);
|
|
783
|
+
if (artifact.source === 'workflow_kit' && existsSync(absPath)) {
|
|
784
|
+
continue;
|
|
870
785
|
}
|
|
786
|
+
const parentDir = dirname(absPath);
|
|
787
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
788
|
+
writeFileSync(absPath, artifact.content);
|
|
871
789
|
}
|
|
872
790
|
|
|
873
791
|
// TALK.md
|
package/src/commands/role.js
CHANGED
|
@@ -37,13 +37,19 @@ function listRoles(roles, roleIds, opts) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (opts.json) {
|
|
40
|
-
const output = roleIds.map((id) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const output = roleIds.map((id) => {
|
|
41
|
+
const entry = {
|
|
42
|
+
id,
|
|
43
|
+
title: roles[id].title,
|
|
44
|
+
mandate: roles[id].mandate,
|
|
45
|
+
write_authority: roles[id].write_authority,
|
|
46
|
+
runtime: roles[id].runtime,
|
|
47
|
+
};
|
|
48
|
+
if (typeof roles[id].decision_authority === 'number') {
|
|
49
|
+
entry.decision_authority = roles[id].decision_authority;
|
|
50
|
+
}
|
|
51
|
+
return entry;
|
|
52
|
+
});
|
|
47
53
|
console.log(JSON.stringify(output, null, 2));
|
|
48
54
|
return;
|
|
49
55
|
}
|
|
@@ -56,7 +62,8 @@ function listRoles(roles, roleIds, opts) {
|
|
|
56
62
|
: r.write_authority === 'proposed'
|
|
57
63
|
? chalk.yellow(r.write_authority)
|
|
58
64
|
: chalk.dim(r.write_authority);
|
|
59
|
-
|
|
65
|
+
const decAuth = typeof r.decision_authority === 'number' ? chalk.dim(` dec:${r.decision_authority}`) : '';
|
|
66
|
+
console.log(` ${chalk.cyan(id)} — ${r.title} [${authority}${decAuth}] → ${chalk.dim(r.runtime)}`);
|
|
60
67
|
}
|
|
61
68
|
console.log('');
|
|
62
69
|
console.log(chalk.dim(' Usage: agentxchain role show <role_id>\n'));
|
|
@@ -81,13 +88,17 @@ function showRole(roleId, roles, roleIds, opts) {
|
|
|
81
88
|
const r = roles[roleId];
|
|
82
89
|
|
|
83
90
|
if (opts.json) {
|
|
84
|
-
|
|
91
|
+
const entry = {
|
|
85
92
|
id: roleId,
|
|
86
93
|
title: r.title,
|
|
87
94
|
mandate: r.mandate,
|
|
88
95
|
write_authority: r.write_authority,
|
|
89
96
|
runtime: r.runtime,
|
|
90
|
-
}
|
|
97
|
+
};
|
|
98
|
+
if (typeof r.decision_authority === 'number') {
|
|
99
|
+
entry.decision_authority = r.decision_authority;
|
|
100
|
+
}
|
|
101
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
91
102
|
return;
|
|
92
103
|
}
|
|
93
104
|
|
|
@@ -101,6 +112,9 @@ function showRole(roleId, roles, roleIds, opts) {
|
|
|
101
112
|
console.log(` Title: ${r.title}`);
|
|
102
113
|
console.log(` Mandate: ${r.mandate}`);
|
|
103
114
|
console.log(` Authority: ${authority}`);
|
|
115
|
+
if (typeof r.decision_authority === 'number') {
|
|
116
|
+
console.log(` Decision: ${r.decision_authority}`);
|
|
117
|
+
}
|
|
104
118
|
console.log(` Runtime: ${chalk.dim(r.runtime)}`);
|
|
105
119
|
console.log('');
|
|
106
120
|
}
|
|
@@ -618,7 +618,7 @@ function renderContext(state, config, root, turn, role) {
|
|
|
618
618
|
|
|
619
619
|
// Repo-level decisions that persist across runs
|
|
620
620
|
if (state.repo_decisions && state.repo_decisions.length > 0) {
|
|
621
|
-
const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions);
|
|
621
|
+
const repoDecMd = renderRepoDecisionsMarkdown(state.repo_decisions, config);
|
|
622
622
|
if (repoDecMd) {
|
|
623
623
|
lines.push(repoDecMd);
|
|
624
624
|
}
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { summarizeRepoDecisions } from './repo-decisions.js';
|
|
5
6
|
|
|
6
7
|
const SUPPORTED_EXPORT_SCHEMA_VERSIONS = new Set(['0.2', '0.3']);
|
|
7
8
|
const VALID_FILE_FORMATS = new Set(['json', 'jsonl', 'text']);
|
|
@@ -369,38 +370,18 @@ function verifyDelegationSummary(artifact, errors) {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
function buildExpectedRepoDecisionsSummary(files) {
|
|
373
|
+
function buildExpectedRepoDecisionsSummary(files, config = null) {
|
|
373
374
|
const repoDecisionsData = files?.['.agentxchain/repo-decisions.jsonl']?.data;
|
|
374
375
|
if (!Array.isArray(repoDecisionsData) || repoDecisionsData.length === 0) {
|
|
375
376
|
return null;
|
|
376
377
|
}
|
|
377
|
-
|
|
378
|
-
const active = repoDecisionsData.filter((d) => d.status === 'active');
|
|
379
|
-
const overridden = repoDecisionsData.filter((d) => d.status === 'overridden');
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
total: repoDecisionsData.length,
|
|
383
|
-
active_count: active.length,
|
|
384
|
-
overridden_count: overridden.length,
|
|
385
|
-
active: active.map((d) => ({
|
|
386
|
-
id: d.id,
|
|
387
|
-
category: d.category,
|
|
388
|
-
statement: d.statement,
|
|
389
|
-
role: d.role,
|
|
390
|
-
run_id: d.run_id,
|
|
391
|
-
})),
|
|
392
|
-
overridden: overridden.map((d) => ({
|
|
393
|
-
id: d.id,
|
|
394
|
-
overridden_by: d.overridden_by,
|
|
395
|
-
statement: d.statement,
|
|
396
|
-
})),
|
|
397
|
-
};
|
|
378
|
+
return summarizeRepoDecisions(repoDecisionsData, config);
|
|
398
379
|
}
|
|
399
380
|
|
|
400
381
|
function verifyRepoDecisionsSummary(artifact, errors) {
|
|
401
382
|
const summary = artifact.summary?.repo_decisions;
|
|
402
383
|
const hasFile = '.agentxchain/repo-decisions.jsonl' in (artifact.files || {});
|
|
403
|
-
const expected = buildExpectedRepoDecisionsSummary(artifact.files);
|
|
384
|
+
const expected = buildExpectedRepoDecisionsSummary(artifact.files, artifact.config || null);
|
|
404
385
|
|
|
405
386
|
if (summary === null && expected === null) {
|
|
406
387
|
return;
|
package/src/lib/export.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-co
|
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
9
|
import { normalizeRunProvenance } from './run-provenance.js';
|
|
10
10
|
import { getDashboardPid, getDashboardSession } from '../commands/dashboard.js';
|
|
11
|
-
import { readRepoDecisions } from './repo-decisions.js';
|
|
11
|
+
import { readRepoDecisions, summarizeRepoDecisions } from './repo-decisions.js';
|
|
12
12
|
import { RUN_EVENTS_PATH } from './run-events.js';
|
|
13
13
|
|
|
14
14
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
@@ -211,18 +211,8 @@ function buildDashboardSessionSummary(root) {
|
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
export function buildRepoDecisionsSummary(root) {
|
|
215
|
-
|
|
216
|
-
if (!all || all.length === 0) return null;
|
|
217
|
-
const active = all.filter(d => d.status === 'active');
|
|
218
|
-
const overridden = all.filter(d => d.status === 'overridden');
|
|
219
|
-
return {
|
|
220
|
-
total: all.length,
|
|
221
|
-
active_count: active.length,
|
|
222
|
-
overridden_count: overridden.length,
|
|
223
|
-
active: active.map(d => ({ id: d.id, category: d.category, statement: d.statement, role: d.role, run_id: d.run_id })),
|
|
224
|
-
overridden: overridden.map(d => ({ id: d.id, overridden_by: d.overridden_by, statement: d.statement })),
|
|
225
|
-
};
|
|
214
|
+
export function buildRepoDecisionsSummary(root, config = null) {
|
|
215
|
+
return summarizeRepoDecisions(readRepoDecisions(root), config);
|
|
226
216
|
}
|
|
227
217
|
|
|
228
218
|
export function buildDelegationSummary(files) {
|
|
@@ -471,7 +461,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
471
461
|
coordinator_present: Object.keys(files).some((path) => path.startsWith('.agentxchain/multirepo/')),
|
|
472
462
|
dashboard_session: buildDashboardSessionSummary(root),
|
|
473
463
|
delegation_summary: buildDelegationSummary(files),
|
|
474
|
-
repo_decisions: buildRepoDecisionsSummary(root),
|
|
464
|
+
repo_decisions: buildRepoDecisionsSummary(root, rawConfig),
|
|
475
465
|
},
|
|
476
466
|
workspace: buildRunWorkspaceMetadata(root),
|
|
477
467
|
files,
|
|
@@ -2437,7 +2437,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2437
2437
|
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
2438
2438
|
for (const dec of turnResult.decisions) {
|
|
2439
2439
|
if (dec.overrides) {
|
|
2440
|
-
const overrideCheck = validateOverride(root, dec);
|
|
2440
|
+
const overrideCheck = validateOverride(root, { ...dec, role: dec.role || turnResult.role }, config);
|
|
2441
2441
|
if (!overrideCheck.ok) {
|
|
2442
2442
|
return {
|
|
2443
2443
|
ok: false,
|
|
@@ -3359,6 +3359,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3359
3359
|
category: dec.category,
|
|
3360
3360
|
statement: dec.statement,
|
|
3361
3361
|
rationale: dec.rationale,
|
|
3362
|
+
durability: dec.durability || 'repo',
|
|
3363
|
+
overrides: dec.overrides || null,
|
|
3362
3364
|
status: 'active',
|
|
3363
3365
|
overridden_by: null,
|
|
3364
3366
|
created_at: now,
|
|
@@ -366,6 +366,11 @@ export function validateV4Config(data, projectRoot) {
|
|
|
366
366
|
if (!VALID_WRITE_AUTHORITIES.includes(role.write_authority)) {
|
|
367
367
|
errors.push(`Role "${id}": write_authority must be one of: ${VALID_WRITE_AUTHORITIES.join(', ')}`);
|
|
368
368
|
}
|
|
369
|
+
if (role.decision_authority !== undefined && role.decision_authority !== null) {
|
|
370
|
+
if (!Number.isInteger(role.decision_authority) || role.decision_authority < 0 || role.decision_authority > 99) {
|
|
371
|
+
errors.push(`Role "${id}": decision_authority must be an integer between 0 and 99`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
369
374
|
if (typeof role.runtime !== 'string' || !role.runtime.trim()) errors.push(`Role "${id}": runtime required`);
|
|
370
375
|
}
|
|
371
376
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { buildSystemSpecContent } from './governed-templates.js';
|
|
3
|
+
|
|
4
|
+
export const GOVERNED_BASELINE_PLANNING_PATHS = Object.freeze([
|
|
5
|
+
'.planning/PM_SIGNOFF.md',
|
|
6
|
+
'.planning/ROADMAP.md',
|
|
7
|
+
'.planning/SYSTEM_SPEC.md',
|
|
8
|
+
'.planning/IMPLEMENTATION_NOTES.md',
|
|
9
|
+
'.planning/acceptance-matrix.md',
|
|
10
|
+
'.planning/ship-verdict.md',
|
|
11
|
+
'.planning/RELEASE_NOTES.md',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const PHASE_DISPLAY_NAMES = Object.freeze({
|
|
15
|
+
qa: 'QA',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function formatPhaseDisplayName(phaseKey) {
|
|
19
|
+
if (PHASE_DISPLAY_NAMES[phaseKey]) {
|
|
20
|
+
return PHASE_DISPLAY_NAMES[phaseKey];
|
|
21
|
+
}
|
|
22
|
+
return phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildRoadmapPhaseTable(routing, roles) {
|
|
26
|
+
const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
|
|
27
|
+
const phaseName = formatPhaseDisplayName(phaseKey);
|
|
28
|
+
const entryRole = phaseConfig.entry_role;
|
|
29
|
+
const role = roles[entryRole];
|
|
30
|
+
const goal = role?.mandate || phaseName;
|
|
31
|
+
const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
|
|
32
|
+
return `| ${phaseName} | ${goal} | ${status} |`;
|
|
33
|
+
});
|
|
34
|
+
return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function interpolateTemplateContent(contentTemplate, projectName) {
|
|
38
|
+
return contentTemplate.replaceAll('{{project_name}}', projectName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function appendAcceptanceHints(baseMatrix, acceptanceHints) {
|
|
42
|
+
if (!Array.isArray(acceptanceHints) || acceptanceHints.length === 0) {
|
|
43
|
+
return baseMatrix;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hintLines = acceptanceHints.map((hint) => `- [ ] ${hint}`).join('\n');
|
|
47
|
+
return `${baseMatrix}\n\n## Template Guidance\n${hintLines}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function generateWorkflowKitPlaceholder(artifact, projectName) {
|
|
51
|
+
const filename = basename(artifact.path);
|
|
52
|
+
const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
53
|
+
|
|
54
|
+
if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
|
|
55
|
+
const sections = artifact.semantics_config.required_sections
|
|
56
|
+
.map((section) => `${section}\n\n(Content here.)\n`)
|
|
57
|
+
.join('\n');
|
|
58
|
+
return `# ${title} — ${projectName}\n\n${sections}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildGovernedPlanningArtifacts({ projectName, routing, roles, template, workflowKitConfig }) {
|
|
65
|
+
const artifacts = [
|
|
66
|
+
{
|
|
67
|
+
path: '.planning/PM_SIGNOFF.md',
|
|
68
|
+
source: 'core',
|
|
69
|
+
content: `# PM Signoff — ${projectName}\n\nApproved: NO\n\n> This scaffold starts blocked on purpose. Change this to \`Approved: YES\` only after a human reviews the planning artifacts and is ready to open the planning gate.\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: '.planning/ROADMAP.md',
|
|
73
|
+
source: 'core',
|
|
74
|
+
content: `# Roadmap — ${projectName}\n\n## Phases\n\n${buildRoadmapPhaseTable(routing, roles)}`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
path: '.planning/SYSTEM_SPEC.md',
|
|
78
|
+
source: 'core',
|
|
79
|
+
content: buildSystemSpecContent(projectName, template?.system_spec_overlay),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: '.planning/IMPLEMENTATION_NOTES.md',
|
|
83
|
+
source: 'core',
|
|
84
|
+
content: `# Implementation Notes — ${projectName}\n\n## Changes\n\n(Dev fills this during implementation)\n\n## Verification\n\n(Dev fills this during implementation)\n\n## Unresolved Follow-ups\n\n(Dev lists any known gaps, tech debt, or follow-up items here.)\n`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: '.planning/acceptance-matrix.md',
|
|
88
|
+
source: 'core',
|
|
89
|
+
content: appendAcceptanceHints(
|
|
90
|
+
`# Acceptance Matrix — ${projectName}\n\n| Req # | Requirement | Acceptance criteria | Test status | Last tested | Status |\n|-------|-------------|-------------------|-------------|-------------|--------|\n| (QA fills this from ROADMAP.md) | | | | | |\n`,
|
|
91
|
+
template?.acceptance_hints,
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
path: '.planning/ship-verdict.md',
|
|
96
|
+
source: 'core',
|
|
97
|
+
content: `# Ship Verdict — ${projectName}\n\n## Verdict: PENDING\n\n## QA Summary\n\n(QA writes the final ship/no-ship assessment here.)\n\n## Open Blockers\n\n(List any blocking issues.)\n\n## Conditions\n\n(List any conditions for shipping.)\n`,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: '.planning/RELEASE_NOTES.md',
|
|
101
|
+
source: 'core',
|
|
102
|
+
content: `# Release Notes — ${projectName}\n\n## User Impact\n\n(QA fills this during the QA phase)\n\n## Verification Summary\n\n(QA fills this during the QA phase)\n\n## Upgrade Notes\n\n(QA fills this during the QA phase)\n\n## Known Issues\n\n(QA fills this during the QA phase)\n`,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const artifact of template?.planning_artifacts || []) {
|
|
107
|
+
artifacts.push({
|
|
108
|
+
path: `.planning/${artifact.filename}`,
|
|
109
|
+
source: 'template',
|
|
110
|
+
content: interpolateTemplateContent(artifact.content_template, projectName),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const seenPaths = new Set(GOVERNED_BASELINE_PLANNING_PATHS);
|
|
115
|
+
if (workflowKitConfig?.phases && typeof workflowKitConfig.phases === 'object') {
|
|
116
|
+
for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
|
|
117
|
+
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
118
|
+
for (const artifact of phaseConfig.artifacts) {
|
|
119
|
+
if (!artifact.path || seenPaths.has(artifact.path)) continue;
|
|
120
|
+
seenPaths.add(artifact.path);
|
|
121
|
+
artifacts.push({
|
|
122
|
+
path: artifact.path,
|
|
123
|
+
source: 'workflow_kit',
|
|
124
|
+
content: generateWorkflowKitPlaceholder(artifact, projectName),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return artifacts;
|
|
131
|
+
}
|
|
@@ -37,6 +37,74 @@ export function getRepoDecisionById(root, decisionId) {
|
|
|
37
37
|
return readRepoDecisions(root).find(d => d.id === decisionId) || null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function getDecisionAuthorityMetadata(roleId, config) {
|
|
41
|
+
const resolved = resolveDecisionAuthority(roleId, config);
|
|
42
|
+
if (resolved === null) return null;
|
|
43
|
+
if (typeof resolved === 'object' && resolved.unknown) {
|
|
44
|
+
return {
|
|
45
|
+
level: resolved.level,
|
|
46
|
+
source: 'unknown_role',
|
|
47
|
+
role: roleId || null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (roleId === 'human') {
|
|
51
|
+
const explicitHumanAuthority = typeof config?.roles?.human?.decision_authority === 'number';
|
|
52
|
+
return {
|
|
53
|
+
level: resolved,
|
|
54
|
+
source: explicitHumanAuthority ? 'configured' : 'human_default',
|
|
55
|
+
role: roleId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
level: resolved,
|
|
60
|
+
source: 'configured',
|
|
61
|
+
role: roleId || null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function summarizeRepoDecisions(decisions, config) {
|
|
66
|
+
if (!Array.isArray(decisions) || decisions.length === 0) return null;
|
|
67
|
+
const active = decisions.filter((d) => d.status === 'active');
|
|
68
|
+
const overridden = decisions.filter((d) => d.status === 'overridden');
|
|
69
|
+
const addAuthority = (decision) => {
|
|
70
|
+
const authority = getDecisionAuthorityMetadata(decision.role, config);
|
|
71
|
+
return {
|
|
72
|
+
id: decision.id,
|
|
73
|
+
category: decision.category,
|
|
74
|
+
statement: decision.statement,
|
|
75
|
+
role: decision.role,
|
|
76
|
+
run_id: decision.run_id,
|
|
77
|
+
overrides: decision.overrides || null,
|
|
78
|
+
durability: decision.durability || 'repo',
|
|
79
|
+
authority_level: authority?.level ?? null,
|
|
80
|
+
authority_source: authority?.source || null,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
total: decisions.length,
|
|
85
|
+
active_count: active.length,
|
|
86
|
+
overridden_count: overridden.length,
|
|
87
|
+
active: active.map(addAuthority),
|
|
88
|
+
overridden: overridden.map((d) => {
|
|
89
|
+
const authority = getDecisionAuthorityMetadata(d.role, config);
|
|
90
|
+
return {
|
|
91
|
+
id: d.id,
|
|
92
|
+
overridden_by: d.overridden_by,
|
|
93
|
+
statement: d.statement,
|
|
94
|
+
overrides: d.overrides || null,
|
|
95
|
+
durability: d.durability || 'repo',
|
|
96
|
+
role: d.role || null,
|
|
97
|
+
authority_level: authority?.level ?? null,
|
|
98
|
+
authority_source: authority?.source || null,
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildRepoDecisionsSummary(decisions) {
|
|
105
|
+
return summarizeRepoDecisions(decisions, null);
|
|
106
|
+
}
|
|
107
|
+
|
|
40
108
|
// ── Write ───────────────────────────────────────────────────────────────────
|
|
41
109
|
|
|
42
110
|
export function appendRepoDecision(root, entry) {
|
|
@@ -62,7 +130,16 @@ export function overrideRepoDecision(root, targetId, overridingId) {
|
|
|
62
130
|
|
|
63
131
|
// ── Validate Override ───────────────────────────────────────────────────────
|
|
64
132
|
|
|
65
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Validate that an override is allowed.
|
|
135
|
+
* @param {string} root - project root
|
|
136
|
+
* @param {object} decision - the overriding decision (must have .overrides, .id, optionally .role)
|
|
137
|
+
* @param {object} [config] - agentxchain config (used for authority enforcement)
|
|
138
|
+
* @returns {{ ok: boolean, error?: string, warning?: string }}
|
|
139
|
+
*
|
|
140
|
+
* DEC-SPEC: .planning/DECISION_AUTHORITY_SPEC.md
|
|
141
|
+
*/
|
|
142
|
+
export function validateOverride(root, decision, config) {
|
|
66
143
|
if (!decision.overrides) return { ok: true };
|
|
67
144
|
const targetId = decision.overrides;
|
|
68
145
|
const target = getRepoDecisionById(root, targetId);
|
|
@@ -75,21 +152,104 @@ export function validateOverride(root, decision) {
|
|
|
75
152
|
if (target.status !== 'active') {
|
|
76
153
|
return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
|
|
77
154
|
}
|
|
155
|
+
|
|
156
|
+
// Authority enforcement (opt-in via decision_authority on roles)
|
|
157
|
+
const authorityResult = checkOverrideAuthority(decision, target, config);
|
|
158
|
+
if (!authorityResult.ok) return authorityResult;
|
|
159
|
+
|
|
160
|
+
return authorityResult.warning ? { ok: true, warning: authorityResult.warning } : { ok: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the decision_authority level for a role.
|
|
165
|
+
* - 'human' defaults to 100 unless explicitly configured.
|
|
166
|
+
* - Unknown roles default to 0 (with warning).
|
|
167
|
+
* - Null means opt-out (no enforcement).
|
|
168
|
+
*/
|
|
169
|
+
export function resolveDecisionAuthority(roleId, config) {
|
|
170
|
+
if (!config || !config.roles) return null;
|
|
171
|
+
if (roleId === 'human') {
|
|
172
|
+
const humanRole = config.roles.human;
|
|
173
|
+
if (humanRole && typeof humanRole.decision_authority === 'number') {
|
|
174
|
+
return humanRole.decision_authority;
|
|
175
|
+
}
|
|
176
|
+
return 100; // human default
|
|
177
|
+
}
|
|
178
|
+
const role = config.roles[roleId];
|
|
179
|
+
if (!role) return { level: 0, unknown: true };
|
|
180
|
+
if (typeof role.decision_authority !== 'number') return null;
|
|
181
|
+
return role.decision_authority;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check whether the overriding role has sufficient authority to override
|
|
186
|
+
* a decision made by the target role.
|
|
187
|
+
*/
|
|
188
|
+
function checkOverrideAuthority(overridingDecision, targetDecision, config) {
|
|
189
|
+
if (!config || !config.roles) return { ok: true };
|
|
190
|
+
|
|
191
|
+
const overridingRole = overridingDecision.role;
|
|
192
|
+
const targetRole = targetDecision.role;
|
|
193
|
+
|
|
194
|
+
// Same-role override is always allowed
|
|
195
|
+
if (overridingRole && targetRole && overridingRole === targetRole) return { ok: true };
|
|
196
|
+
|
|
197
|
+
const targetAuth = resolveDecisionAuthority(targetRole, config);
|
|
198
|
+
const overridingAuth = resolveDecisionAuthority(overridingRole, config);
|
|
199
|
+
|
|
200
|
+
// Handle unknown target role
|
|
201
|
+
let warning;
|
|
202
|
+
if (targetAuth && typeof targetAuth === 'object' && targetAuth.unknown) {
|
|
203
|
+
warning = `decisions: target decision role '${targetRole}' not found in current config, treating as authority 0.`;
|
|
204
|
+
// targetAuth is effectively 0, allow override
|
|
205
|
+
return { ok: true, warning };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Opt-in: if either side is null (not configured), allow
|
|
209
|
+
if (targetAuth === null || overridingAuth === null) return { ok: true };
|
|
210
|
+
|
|
211
|
+
// Handle unknown overriding role (shouldn't normally happen, but be safe)
|
|
212
|
+
const overridingLevel = (typeof overridingAuth === 'object' && overridingAuth.unknown) ? 0 : overridingAuth;
|
|
213
|
+
const targetLevel = (typeof targetAuth === 'object') ? 0 : targetAuth;
|
|
214
|
+
|
|
215
|
+
if (overridingLevel < targetLevel) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: `decisions: role '${overridingRole}' (authority ${overridingLevel}) cannot override ${targetDecision.id} made by '${targetRole}' (authority ${targetLevel}). Override requires authority >= ${targetLevel}.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
78
222
|
return { ok: true };
|
|
79
223
|
}
|
|
80
224
|
|
|
81
225
|
// ── Render ──────────────────────────────────────────────────────────────────
|
|
82
226
|
|
|
83
|
-
export function renderRepoDecisionsMarkdown(activeDecisions) {
|
|
227
|
+
export function renderRepoDecisionsMarkdown(activeDecisions, config) {
|
|
84
228
|
if (!activeDecisions || activeDecisions.length === 0) return '';
|
|
229
|
+
const hasAuthorityPolicy = Object.values(config?.roles || {}).some((role) => (
|
|
230
|
+
role && typeof role.decision_authority === 'number'
|
|
231
|
+
));
|
|
85
232
|
const lines = [
|
|
86
233
|
'## Active Repo Decisions',
|
|
87
234
|
'',
|
|
88
235
|
'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
|
|
89
236
|
'',
|
|
90
237
|
];
|
|
238
|
+
if (hasAuthorityPolicy) {
|
|
239
|
+
lines.push('When both roles declare `decision_authority`, overrides require authority greater than or equal to the originating role.');
|
|
240
|
+
lines.push('');
|
|
241
|
+
}
|
|
91
242
|
for (const d of activeDecisions) {
|
|
92
|
-
|
|
243
|
+
const authority = getDecisionAuthorityMetadata(d.role, config);
|
|
244
|
+
const authorityText = authority
|
|
245
|
+
? authority.source === 'human_default'
|
|
246
|
+
? ' authority 100 (human default)'
|
|
247
|
+
: authority.source === 'unknown_role'
|
|
248
|
+
? ' authority 0 (role no longer in config)'
|
|
249
|
+
: ` authority ${authority.level}`
|
|
250
|
+
: '';
|
|
251
|
+
const supersedes = d.overrides ? ` Supersedes ${d.overrides}.` : '';
|
|
252
|
+
lines.push(`- **${d.id}** (${d.category}, by ${d.role || 'unknown'}${authorityText}): ${d.statement}${supersedes}`);
|
|
93
253
|
}
|
|
94
254
|
lines.push('');
|
|
95
255
|
return lines.join('\n');
|
package/src/lib/report.js
CHANGED
|
@@ -1320,7 +1320,13 @@ export function formatGovernanceReportText(report) {
|
|
|
1320
1320
|
lines.push('', 'Repo Decisions:');
|
|
1321
1321
|
lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
|
|
1322
1322
|
for (const d of run.repo_decisions.active) {
|
|
1323
|
-
|
|
1323
|
+
const supersedes = d.overrides ? ` | supersedes ${d.overrides}` : '';
|
|
1324
|
+
const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1325
|
+
lines.push(` - ${d.id} (${d.category}): ${d.statement}${supersedes}${authority}`);
|
|
1326
|
+
}
|
|
1327
|
+
for (const d of run.repo_decisions.overridden || []) {
|
|
1328
|
+
const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1329
|
+
lines.push(` - ${d.id} (overridden by ${d.overridden_by || 'unknown'}${authority})`);
|
|
1324
1330
|
}
|
|
1325
1331
|
}
|
|
1326
1332
|
|
|
@@ -1825,10 +1831,20 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1825
1831
|
if (run.repo_decisions?.active?.length > 0) {
|
|
1826
1832
|
lines.push('', '## Repo Decisions', '');
|
|
1827
1833
|
lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
|
|
1828
|
-
lines.push('| ID | Category | Statement | Role | Run |', '
|
|
1834
|
+
lines.push('| ID | Category | Statement | Role | Authority | Run | Supersedes |', '|----|----------|-----------|------|-----------|-----|------------|');
|
|
1829
1835
|
for (const d of run.repo_decisions.active) {
|
|
1830
1836
|
const stmt = (d.statement || '').replace(/\|/g, '\\|');
|
|
1831
|
-
|
|
1837
|
+
const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1838
|
+
lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | ${authority} | \`${(d.run_id || '').slice(0, 12)}\` | ${d.overrides || '—'} |`);
|
|
1839
|
+
}
|
|
1840
|
+
if (run.repo_decisions.overridden?.length > 0) {
|
|
1841
|
+
lines.push('', 'Overridden decisions:', '');
|
|
1842
|
+
lines.push('| ID | Statement | Authority | Overridden By |', '|----|-----------|-----------|---------------|');
|
|
1843
|
+
for (const d of run.repo_decisions.overridden) {
|
|
1844
|
+
const stmt = (d.statement || '').replace(/\|/g, '\\|');
|
|
1845
|
+
const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1846
|
+
lines.push(`| ${d.id} | ${stmt} | ${authority} | ${d.overridden_by || '—'} |`);
|
|
1847
|
+
}
|
|
1832
1848
|
}
|
|
1833
1849
|
}
|
|
1834
1850
|
|
|
@@ -2453,9 +2469,28 @@ function renderRunHtml(report) {
|
|
|
2453
2469
|
if (run.repo_decisions?.active?.length > 0) {
|
|
2454
2470
|
let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
|
|
2455
2471
|
rdHtml += htmlTable(
|
|
2456
|
-
['ID', 'Category', 'Statement', 'Role', 'Run'],
|
|
2457
|
-
run.repo_decisions.active.map((d) => [
|
|
2472
|
+
['ID', 'Category', 'Statement', 'Role', 'Authority', 'Run', 'Supersedes'],
|
|
2473
|
+
run.repo_decisions.active.map((d) => [
|
|
2474
|
+
esc(d.id),
|
|
2475
|
+
esc(d.category),
|
|
2476
|
+
esc(d.statement || ''),
|
|
2477
|
+
esc(d.role || '\u2014'),
|
|
2478
|
+
esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
|
|
2479
|
+
`<code>${esc((d.run_id || '').slice(0, 12))}</code>`,
|
|
2480
|
+
esc(d.overrides || '\u2014'),
|
|
2481
|
+
]),
|
|
2458
2482
|
);
|
|
2483
|
+
if (run.repo_decisions.overridden?.length > 0) {
|
|
2484
|
+
rdHtml += htmlTable(
|
|
2485
|
+
['ID', 'Statement', 'Authority', 'Overridden By'],
|
|
2486
|
+
run.repo_decisions.overridden.map((d) => [
|
|
2487
|
+
esc(d.id),
|
|
2488
|
+
esc(d.statement || ''),
|
|
2489
|
+
esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
|
|
2490
|
+
esc(d.overridden_by || '\u2014'),
|
|
2491
|
+
]),
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2459
2494
|
sections.push(`<div class="section">${htmlSection('Repo Decisions', rdHtml)}</div>`);
|
|
2460
2495
|
}
|
|
2461
2496
|
|