agentxchain 2.38.0 → 2.40.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 -0
- package/bin/agentxchain.js +1 -0
- package/dashboard/app.js +2 -1
- package/dashboard/components/gate.js +2 -1
- package/dashboard/components/timeline.js +55 -1
- package/package.json +2 -1
- package/scripts/verify-post-publish.sh +76 -0
- package/src/commands/init.js +10 -7
- package/src/commands/status.js +71 -0
- package/src/commands/template-list.js +41 -0
- package/src/lib/connector-health.js +319 -0
- package/src/lib/dashboard/bridge-server.js +7 -0
- package/src/lib/dashboard/connectors.js +41 -0
- package/src/lib/dashboard/workflow-kit-artifacts.js +14 -27
- package/src/lib/normalized-config.js +52 -26
- package/src/lib/workflow-kit-artifacts.js +36 -0
- package/src/lib/workflow-kit-phase-templates.js +128 -0
- package/src/templates/governed/enterprise-app.json +5 -21
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
|
|
|
9
9
|
## Docs
|
|
10
10
|
|
|
11
11
|
- [Quickstart](https://agentxchain.dev/docs/quickstart/)
|
|
12
|
+
- [Getting Started](https://agentxchain.dev/docs/getting-started/)
|
|
12
13
|
- [CLI reference](https://agentxchain.dev/docs/cli/)
|
|
14
|
+
- [Templates](https://agentxchain.dev/docs/templates/)
|
|
13
15
|
- [Export schema reference](https://agentxchain.dev/docs/export-schema/)
|
|
14
16
|
- [Adapter reference](https://agentxchain.dev/docs/adapters/)
|
|
15
17
|
- [Protocol spec (v6)](https://agentxchain.dev/docs/protocol/)
|
|
@@ -90,6 +92,15 @@ Built-in governed templates:
|
|
|
90
92
|
- `web-app`: user flows, UI acceptance, browser support
|
|
91
93
|
- `enterprise-app`: enterprise planning artifacts plus blueprint-backed `architect` and `security_reviewer` phases
|
|
92
94
|
|
|
95
|
+
Inspect the shipped template surfaces instead of inferring them from docs:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
agentxchain template list
|
|
99
|
+
agentxchain template list --phase-templates
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`template list` enumerates governed project templates. `template list --phase-templates` enumerates the reusable workflow-kit phase-template bundles you can reference from `workflow_kit.phases.<phase>.template`.
|
|
103
|
+
|
|
93
104
|
`step` writes a turn-scoped bundle under `.agentxchain/dispatch/turns/<turn_id>/` and expects a staged result at `.agentxchain/staging/<turn_id>/turn-result.json`. Typical continuation:
|
|
94
105
|
|
|
95
106
|
```bash
|
package/bin/agentxchain.js
CHANGED
|
@@ -411,6 +411,7 @@ templateCmd
|
|
|
411
411
|
.command('list')
|
|
412
412
|
.description('List available governed templates')
|
|
413
413
|
.option('-j, --json', 'Output as JSON')
|
|
414
|
+
.option('--phase-templates', 'List workflow-kit phase templates instead of governed project templates')
|
|
414
415
|
.action(templateListCommand);
|
|
415
416
|
|
|
416
417
|
templateCmd
|
package/dashboard/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { render as renderBlockers } from './components/blockers.js';
|
|
|
16
16
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
17
17
|
|
|
18
18
|
const VIEWS = {
|
|
19
|
-
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations'], render: renderTimeline },
|
|
19
|
+
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors'], render: renderTimeline },
|
|
20
20
|
ledger: { fetch: ['ledger'], render: renderLedger },
|
|
21
21
|
hooks: { fetch: ['audit', 'annotations'], render: renderHooks },
|
|
22
22
|
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
@@ -41,6 +41,7 @@ const API_MAP = {
|
|
|
41
41
|
coordinatorAudit: '/api/coordinator/hooks/audit',
|
|
42
42
|
coordinatorBlockers: '/api/coordinator/blockers',
|
|
43
43
|
workflowKitArtifacts: '/api/workflow-kit-artifacts',
|
|
44
|
+
connectors: '/api/connectors',
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const viewState = {
|
|
@@ -82,7 +82,8 @@ function aggregateEvidence(turns) {
|
|
|
82
82
|
allDecisions.push(dec);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
const
|
|
85
|
+
const observed = turn.observed_artifact?.files_changed;
|
|
86
|
+
const files = (Array.isArray(observed) && observed.length > 0) ? observed : (turn.files_changed || []);
|
|
86
87
|
for (const f of files) {
|
|
87
88
|
if (!allFiles.includes(f)) {
|
|
88
89
|
allFiles.push(f);
|
|
@@ -177,7 +177,60 @@ function renderContinuityPanel(continuity) {
|
|
|
177
177
|
return html;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
function connectorBadge(state) {
|
|
181
|
+
const colors = {
|
|
182
|
+
healthy: 'var(--green)',
|
|
183
|
+
failing: 'var(--red)',
|
|
184
|
+
active: 'var(--yellow)',
|
|
185
|
+
never_used: 'var(--text-dim)',
|
|
186
|
+
};
|
|
187
|
+
const color = colors[state] || 'var(--text-dim)';
|
|
188
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(state || 'unknown')}</span>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderConnectorHealthPanel(connectorsPayload) {
|
|
192
|
+
const connectors = Array.isArray(connectorsPayload?.connectors)
|
|
193
|
+
? connectorsPayload.connectors
|
|
194
|
+
: [];
|
|
195
|
+
if (connectors.length === 0) return '';
|
|
196
|
+
|
|
197
|
+
let html = `<div class="section"><h3>Connector Health</h3><div class="turn-list">`;
|
|
198
|
+
for (const connector of connectors) {
|
|
199
|
+
html += `<div class="turn-card">
|
|
200
|
+
<div class="turn-header">
|
|
201
|
+
<span class="mono">${esc(connector.runtime_id)}</span>
|
|
202
|
+
${connectorBadge(connector.state)}
|
|
203
|
+
</div>
|
|
204
|
+
<div class="turn-detail"><span class="detail-label">Type:</span> ${esc(connector.type || 'unknown')}</div>
|
|
205
|
+
<div class="turn-detail"><span class="detail-label">Target:</span> <span class="mono">${esc(connector.target || 'unknown')}</span></div>
|
|
206
|
+
<div class="turn-detail"><span class="detail-label">Reachable:</span> ${esc(connector.reachable || 'unknown')}</div>`;
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(connector.active_turn_ids) && connector.active_turn_ids.length > 0) {
|
|
209
|
+
html += `<div class="turn-detail"><span class="detail-label">Active turns:</span> <span class="mono">${esc(connector.active_turn_ids.join(', '))}</span></div>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (connector.last_success_at) {
|
|
213
|
+
html += `<div class="turn-detail"><span class="detail-label">Last success:</span> ${esc(connector.last_success_at)}</div>`;
|
|
214
|
+
}
|
|
215
|
+
if (connector.last_failure_at) {
|
|
216
|
+
html += `<div class="turn-detail"><span class="detail-label">Last failure:</span> ${esc(connector.last_failure_at)}</div>`;
|
|
217
|
+
}
|
|
218
|
+
if (connector.last_error) {
|
|
219
|
+
html += `<div class="turn-detail risks"><span class="detail-label">Last error:</span> ${esc(connector.last_error)}</div>`;
|
|
220
|
+
}
|
|
221
|
+
if (connector.attempts_made != null || connector.latency_ms != null) {
|
|
222
|
+
const attempts = connector.attempts_made != null ? connector.attempts_made : 'n/a';
|
|
223
|
+
const latency = connector.latency_ms != null ? `${connector.latency_ms}ms` : 'n/a';
|
|
224
|
+
html += `<div class="turn-detail"><span class="detail-label">Attempt telemetry:</span> attempts ${esc(attempts)} / latency ${esc(latency)}</div>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
html += `</div>`;
|
|
228
|
+
}
|
|
229
|
+
html += `</div></div>`;
|
|
230
|
+
return html;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function render({ state, continuity, history, annotations, audit, connectors }) {
|
|
181
234
|
if (!state) {
|
|
182
235
|
return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
|
|
183
236
|
}
|
|
@@ -198,6 +251,7 @@ export function render({ state, continuity, history, annotations, audit }) {
|
|
|
198
251
|
</div>`;
|
|
199
252
|
|
|
200
253
|
html += renderContinuityPanel(continuity);
|
|
254
|
+
html += renderConnectorHealthPanel(connectors);
|
|
201
255
|
|
|
202
256
|
// Active turns
|
|
203
257
|
if (activeTurns.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"postflight:downstream": "bash scripts/release-downstream-truth.sh",
|
|
31
31
|
"bump:release": "bash scripts/release-bump.sh",
|
|
32
32
|
"sync:homebrew": "bash scripts/sync-homebrew.sh",
|
|
33
|
+
"verify:post-publish": "bash scripts/verify-post-publish.sh",
|
|
33
34
|
"build:macos": "bun build bin/agentxchain.js --compile --target=bun-darwin-arm64 --outfile=dist/agentxchain-macos-arm64",
|
|
34
35
|
"build:linux": "bun build bin/agentxchain.js --compile --target=bun-linux-x64 --outfile=dist/agentxchain-linux-x64",
|
|
35
36
|
"publish:npm": "bash scripts/publish-npm.sh"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-publish repo-mirror verification — the executable contract that
|
|
3
|
+
# ensures main is green after a release publish completes.
|
|
4
|
+
#
|
|
5
|
+
# Three-phase Homebrew lifecycle:
|
|
6
|
+
# Phase 1 (pre-publish): formula URL updated, SHA carried from previous version
|
|
7
|
+
# Phase 2 (post-publish): npm live, but repo mirror SHA is stale
|
|
8
|
+
# Phase 3 (post-sync): repo mirror SHA matches published tarball — main is green
|
|
9
|
+
#
|
|
10
|
+
# This script transitions from Phase 2 → Phase 3 and verifies the result.
|
|
11
|
+
#
|
|
12
|
+
# Usage: bash scripts/verify-post-publish.sh [--target-version <semver>]
|
|
13
|
+
# If --target-version is omitted, reads from package.json.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
CLI_DIR="${SCRIPT_DIR}/.."
|
|
18
|
+
cd "$CLI_DIR"
|
|
19
|
+
|
|
20
|
+
TARGET_VERSION=""
|
|
21
|
+
|
|
22
|
+
while [[ $# -gt 0 ]]; do
|
|
23
|
+
case "$1" in
|
|
24
|
+
--target-version)
|
|
25
|
+
if [[ -z "${2:-}" ]]; then
|
|
26
|
+
echo "Error: --target-version requires a semver argument" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
TARGET_VERSION="$2"
|
|
30
|
+
shift 2
|
|
31
|
+
;;
|
|
32
|
+
*)
|
|
33
|
+
echo "Unknown argument: $1" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [[ -z "$TARGET_VERSION" ]]; then
|
|
40
|
+
TARGET_VERSION="$(node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).version)")"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo "Post-Publish Verification: v${TARGET_VERSION}"
|
|
44
|
+
echo "============================================="
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
# Step 1: Verify npm serves the version
|
|
48
|
+
echo "[1/4] Checking npm registry..."
|
|
49
|
+
NPM_VERSION="$(npm view "agentxchain@${TARGET_VERSION}" version 2>/dev/null || echo "")"
|
|
50
|
+
if [[ "$NPM_VERSION" != "$TARGET_VERSION" ]]; then
|
|
51
|
+
echo " FAIL: npm does not serve agentxchain@${TARGET_VERSION} (got: '${NPM_VERSION}')"
|
|
52
|
+
echo " This script must run AFTER npm publish completes."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
echo " OK: npm serves v${TARGET_VERSION}"
|
|
56
|
+
|
|
57
|
+
# Step 2: Sync the repo mirror to the published tarball
|
|
58
|
+
echo "[2/4] Syncing repo mirror to published tarball..."
|
|
59
|
+
bash "${SCRIPT_DIR}/sync-homebrew.sh" --target-version "$TARGET_VERSION"
|
|
60
|
+
echo " OK: repo mirror synced"
|
|
61
|
+
|
|
62
|
+
# Step 3: Run the full test suite WITHOUT the preflight skip
|
|
63
|
+
echo "[3/4] Running full test suite (no preflight skip)..."
|
|
64
|
+
echo " This verifies the Homebrew mirror contract passes with the real SHA."
|
|
65
|
+
npm test
|
|
66
|
+
echo " OK: full test suite green"
|
|
67
|
+
|
|
68
|
+
# Step 4: Summary
|
|
69
|
+
echo ""
|
|
70
|
+
echo "============================================="
|
|
71
|
+
echo "Post-publish verification PASSED."
|
|
72
|
+
echo " - npm: agentxchain@${TARGET_VERSION} live"
|
|
73
|
+
echo " - repo mirror: SHA synced to published tarball"
|
|
74
|
+
echo " - test suite: green without preflight skip"
|
|
75
|
+
echo ""
|
|
76
|
+
echo "Main is now in Phase 3 (post-sync). Commit and push the mirror update."
|
package/src/commands/init.js
CHANGED
|
@@ -6,7 +6,7 @@ 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, buildSystemSpecContent } from '../lib/governed-templates.js';
|
|
9
|
-
import { VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
9
|
+
import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@@ -628,6 +628,9 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
628
628
|
const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
|
|
629
629
|
const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
|
|
630
630
|
const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
|
|
631
|
+
const scaffoldWorkflowKitConfig = effectiveWorkflowKitConfig
|
|
632
|
+
? normalizeWorkflowKit(effectiveWorkflowKitConfig, Object.keys(routing))
|
|
633
|
+
: null;
|
|
631
634
|
const initialPhase = Object.keys(routing)[0] || 'planning';
|
|
632
635
|
const phaseGateStatus = Object.fromEntries(
|
|
633
636
|
[...new Set(
|
|
@@ -709,7 +712,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
709
712
|
const basePrompt = buildGovernedPrompt(roleId, role, {
|
|
710
713
|
routing,
|
|
711
714
|
gates,
|
|
712
|
-
workflowKitConfig:
|
|
715
|
+
workflowKitConfig: scaffoldWorkflowKitConfig,
|
|
713
716
|
});
|
|
714
717
|
const prompt = appendPromptOverride(basePrompt, template.prompt_overrides?.[roleId]);
|
|
715
718
|
writeFileSync(join(dir, '.agentxchain', 'prompts', `${roleId}.md`), prompt);
|
|
@@ -736,7 +739,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
736
739
|
|
|
737
740
|
// Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
|
|
738
741
|
// that are not already handled by the default scaffold above
|
|
739
|
-
if (
|
|
742
|
+
if (scaffoldWorkflowKitConfig && scaffoldWorkflowKitConfig.phases && typeof scaffoldWorkflowKitConfig.phases === 'object') {
|
|
740
743
|
const defaultScaffoldPaths = new Set([
|
|
741
744
|
'.planning/PM_SIGNOFF.md',
|
|
742
745
|
'.planning/ROADMAP.md',
|
|
@@ -747,7 +750,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
747
750
|
'.planning/RELEASE_NOTES.md',
|
|
748
751
|
]);
|
|
749
752
|
|
|
750
|
-
for (const phaseConfig of Object.values(
|
|
753
|
+
for (const phaseConfig of Object.values(scaffoldWorkflowKitConfig.phases)) {
|
|
751
754
|
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
752
755
|
for (const artifact of phaseConfig.artifacts) {
|
|
753
756
|
if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
|
|
@@ -782,7 +785,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
782
785
|
}
|
|
783
786
|
}
|
|
784
787
|
|
|
785
|
-
return { config, state };
|
|
788
|
+
return { config, state, scaffoldWorkflowKitConfig };
|
|
786
789
|
}
|
|
787
790
|
|
|
788
791
|
async function initGoverned(opts) {
|
|
@@ -882,7 +885,7 @@ async function initGoverned(opts) {
|
|
|
882
885
|
}
|
|
883
886
|
}
|
|
884
887
|
|
|
885
|
-
const { config } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
|
|
888
|
+
const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
|
|
886
889
|
|
|
887
890
|
console.log('');
|
|
888
891
|
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
@@ -897,7 +900,7 @@ async function initGoverned(opts) {
|
|
|
897
900
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} reviews/`);
|
|
898
901
|
console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} dispatch/`);
|
|
899
902
|
console.log(` ${chalk.dim('├──')} .planning/`);
|
|
900
|
-
const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate,
|
|
903
|
+
const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate, scaffoldWorkflowKitConfig);
|
|
901
904
|
for (const [index, line] of planningSummaryLines.entries()) {
|
|
902
905
|
const branch = index === planningSummaryLines.length - 1 ? '└──' : '├──';
|
|
903
906
|
console.log(` ${chalk.dim('│')} ${chalk.dim(branch)} ${line}`);
|
package/src/commands/status.js
CHANGED
|
@@ -3,6 +3,8 @@ import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState }
|
|
|
3
3
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
4
4
|
import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
5
5
|
import { getContinuityStatus } from '../lib/continuity-status.js';
|
|
6
|
+
import { getConnectorHealth } from '../lib/connector-health.js';
|
|
7
|
+
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
6
8
|
|
|
7
9
|
export async function statusCommand(opts) {
|
|
8
10
|
const context = loadProjectContext();
|
|
@@ -76,6 +78,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
76
78
|
const { root, config, version } = context;
|
|
77
79
|
const state = loadProjectState(root, config);
|
|
78
80
|
const continuity = getContinuityStatus(root, state);
|
|
81
|
+
const connectorHealth = getConnectorHealth(root, config, state);
|
|
82
|
+
|
|
83
|
+
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
79
84
|
|
|
80
85
|
if (opts.json) {
|
|
81
86
|
console.log(JSON.stringify({
|
|
@@ -85,6 +90,8 @@ function renderGovernedStatus(context, opts) {
|
|
|
85
90
|
config,
|
|
86
91
|
state,
|
|
87
92
|
continuity,
|
|
93
|
+
connector_health: connectorHealth,
|
|
94
|
+
workflow_kit_artifacts: workflowKitArtifacts,
|
|
88
95
|
}, null, 2));
|
|
89
96
|
return;
|
|
90
97
|
}
|
|
@@ -105,6 +112,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
105
112
|
console.log('');
|
|
106
113
|
|
|
107
114
|
renderContinuityStatus(continuity, state);
|
|
115
|
+
renderConnectorHealthStatus(connectorHealth);
|
|
108
116
|
|
|
109
117
|
const activeTurnCount = getActiveTurnCount(state);
|
|
110
118
|
const activeTurns = getActiveTurns(state);
|
|
@@ -227,6 +235,8 @@ function renderGovernedStatus(context, opts) {
|
|
|
227
235
|
}
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
renderWorkflowKitArtifactsSection(workflowKitArtifacts);
|
|
239
|
+
|
|
230
240
|
if (state?.budget_status) {
|
|
231
241
|
console.log('');
|
|
232
242
|
console.log(` ${chalk.dim('Budget:')} spent $${formatUsd(state.budget_status.spent_usd)} / remaining $${formatUsd(state.budget_status.remaining_usd)}`);
|
|
@@ -243,6 +253,48 @@ function renderGovernedStatus(context, opts) {
|
|
|
243
253
|
console.log('');
|
|
244
254
|
}
|
|
245
255
|
|
|
256
|
+
function renderConnectorHealthStatus(connectorHealth) {
|
|
257
|
+
const connectors = Array.isArray(connectorHealth?.connectors)
|
|
258
|
+
? connectorHealth.connectors
|
|
259
|
+
: [];
|
|
260
|
+
if (connectors.length === 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(` ${chalk.dim('Connectors:')}`);
|
|
265
|
+
for (const connector of connectors) {
|
|
266
|
+
const stateLabel = formatConnectorState(connector.state);
|
|
267
|
+
console.log(` ${stateLabel} ${chalk.bold(connector.runtime_id)} — ${connector.type} (${connector.target})`);
|
|
268
|
+
|
|
269
|
+
if (connector.active_turn_ids.length > 0) {
|
|
270
|
+
console.log(` ${chalk.dim('Active turns:')} ${connector.active_turn_ids.join(', ')}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (connector.last_error) {
|
|
274
|
+
console.log(` ${chalk.dim('Last error:')} ${connector.last_error}`);
|
|
275
|
+
} else if (connector.last_success_at) {
|
|
276
|
+
console.log(` ${chalk.dim('Last success:')} ${connector.last_success_at}`);
|
|
277
|
+
} else if (connector.last_attempt_at) {
|
|
278
|
+
console.log(` ${chalk.dim('Last attempt:')} ${connector.last_attempt_at}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
console.log('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatConnectorState(state) {
|
|
285
|
+
switch (state) {
|
|
286
|
+
case 'healthy':
|
|
287
|
+
return chalk.green('● healthy');
|
|
288
|
+
case 'failing':
|
|
289
|
+
return chalk.red('✗ failing');
|
|
290
|
+
case 'active':
|
|
291
|
+
return chalk.yellow('● active');
|
|
292
|
+
case 'never_used':
|
|
293
|
+
default:
|
|
294
|
+
return chalk.dim('○ never_used');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
246
298
|
function renderContinuityStatus(continuity, state) {
|
|
247
299
|
if (!continuity) return;
|
|
248
300
|
|
|
@@ -292,6 +344,25 @@ function renderContinuityStatus(continuity, state) {
|
|
|
292
344
|
console.log('');
|
|
293
345
|
}
|
|
294
346
|
|
|
347
|
+
function renderWorkflowKitArtifactsSection(wkData) {
|
|
348
|
+
if (!wkData || !wkData.artifacts || wkData.artifacts.length === 0) return;
|
|
349
|
+
|
|
350
|
+
const artifacts = wkData.artifacts;
|
|
351
|
+
console.log('');
|
|
352
|
+
console.log(` ${chalk.dim('Artifacts:')} (${wkData.phase})`);
|
|
353
|
+
for (const a of artifacts) {
|
|
354
|
+
const icon = a.exists ? chalk.green('✓') : (a.required ? chalk.red('✗') : chalk.yellow('○'));
|
|
355
|
+
const reqLabel = a.required ? '' : chalk.dim(' (optional)');
|
|
356
|
+
const ownerLabel = a.owned_by
|
|
357
|
+
? chalk.dim(` [${a.owned_by}${a.owner_resolution === 'entry_role' ? '*' : ''}]`)
|
|
358
|
+
: '';
|
|
359
|
+
console.log(` ${icon} ${a.path}${ownerLabel}${reqLabel}`);
|
|
360
|
+
}
|
|
361
|
+
if (artifacts.some(a => a.owner_resolution === 'entry_role')) {
|
|
362
|
+
console.log(` ${chalk.dim('* = ownership inferred from entry_role')}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
295
366
|
function formatPhase(phase) {
|
|
296
367
|
const colors = { discovery: chalk.blue, build: chalk.green, qa: chalk.yellow, deploy: chalk.magenta, blocked: chalk.red };
|
|
297
368
|
return (colors[phase] || chalk.white)(phase);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadAllGovernedTemplates, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
|
|
3
|
+
import { listWorkflowKitPhaseTemplates } from '../lib/workflow-kit-phase-templates.js';
|
|
3
4
|
|
|
4
5
|
export function templateListCommand(opts) {
|
|
6
|
+
if (opts.phaseTemplates) {
|
|
7
|
+
return listPhaseTemplates(opts);
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
if (opts.json) {
|
|
6
11
|
const templates = loadAllGovernedTemplates();
|
|
7
12
|
const output = templates.map((t) => ({
|
|
@@ -34,4 +39,40 @@ export function templateListCommand(opts) {
|
|
|
34
39
|
console.log('');
|
|
35
40
|
}
|
|
36
41
|
console.log(chalk.dim(` Usage: agentxchain template set <id>\n`));
|
|
42
|
+
console.log(chalk.dim(` Tip: use --phase-templates to list workflow-kit phase templates.\n`));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listPhaseTemplates(opts) {
|
|
46
|
+
const templates = listWorkflowKitPhaseTemplates();
|
|
47
|
+
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
const output = templates.map((t) => ({
|
|
50
|
+
id: t.id,
|
|
51
|
+
description: t.description,
|
|
52
|
+
artifacts: t.artifacts.map((a) => ({
|
|
53
|
+
path: a.path,
|
|
54
|
+
semantics: a.semantics || null,
|
|
55
|
+
semantics_config: a.semantics_config || null,
|
|
56
|
+
required: a.required,
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
console.log(JSON.stringify(output, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.bold('\n Workflow-kit phase templates:\n'));
|
|
64
|
+
for (const t of templates) {
|
|
65
|
+
console.log(` ${chalk.cyan(t.id)} — ${t.description}`);
|
|
66
|
+
for (const a of t.artifacts) {
|
|
67
|
+
const req = a.required ? chalk.green('required') : chalk.dim('optional');
|
|
68
|
+
const sem = a.semantics ? chalk.yellow(a.semantics) : chalk.dim('none');
|
|
69
|
+
console.log(` ${a.path} [${req}] [semantics: ${sem}]`);
|
|
70
|
+
if (a.semantics === 'section_check' && a.semantics_config?.required_sections) {
|
|
71
|
+
console.log(` sections: ${a.semantics_config.required_sections.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.dim(' Usage in agentxchain.json:'));
|
|
77
|
+
console.log(chalk.dim(' "workflow_kit": { "phases": { "<phase>": { "template": "<id>" } } }\n'));
|
|
37
78
|
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
5
|
+
const STAGING_DIR = '.agentxchain/staging';
|
|
6
|
+
const CONNECTOR_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
7
|
+
|
|
8
|
+
function safeReadJson(absPath) {
|
|
9
|
+
try {
|
|
10
|
+
if (!existsSync(absPath)) return null;
|
|
11
|
+
return JSON.parse(readFileSync(absPath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeReadJsonl(absPath) {
|
|
18
|
+
try {
|
|
19
|
+
if (!existsSync(absPath)) return [];
|
|
20
|
+
const raw = readFileSync(absPath, 'utf8').trim();
|
|
21
|
+
if (!raw) return [];
|
|
22
|
+
return raw.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toMillis(value) {
|
|
29
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
30
|
+
const ms = Date.parse(value);
|
|
31
|
+
return Number.isFinite(ms) ? ms : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function chooseLatest(left, right) {
|
|
35
|
+
if (!left) return right || null;
|
|
36
|
+
if (!right) return left;
|
|
37
|
+
const leftMs = toMillis(left.at);
|
|
38
|
+
const rightMs = toMillis(right.at);
|
|
39
|
+
if (leftMs === null) return right;
|
|
40
|
+
if (rightMs === null) return left;
|
|
41
|
+
return rightMs >= leftMs ? right : left;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function chooseLatestDefined(left, right) {
|
|
45
|
+
if (right == null) return left ?? null;
|
|
46
|
+
if (left == null) return right;
|
|
47
|
+
return right;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatCommand(command, args = []) {
|
|
51
|
+
if (Array.isArray(command)) {
|
|
52
|
+
return command.join(' ');
|
|
53
|
+
}
|
|
54
|
+
if (typeof command === 'string' && command.length > 0) {
|
|
55
|
+
return [command, ...(Array.isArray(args) ? args : [])].join(' ');
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatTarget(runtime) {
|
|
61
|
+
switch (runtime?.type) {
|
|
62
|
+
case 'api_proxy':
|
|
63
|
+
return [runtime.provider, runtime.model].filter(Boolean).join(' / ') || 'unknown target';
|
|
64
|
+
case 'remote_agent':
|
|
65
|
+
return runtime.url || 'unknown target';
|
|
66
|
+
case 'mcp':
|
|
67
|
+
return runtime.transport === 'streamable_http'
|
|
68
|
+
? (runtime.url || 'unknown target')
|
|
69
|
+
: (formatCommand(runtime.command, runtime.args) || 'unknown target');
|
|
70
|
+
case 'local_cli':
|
|
71
|
+
return formatCommand(runtime.command, runtime.args) || 'unknown target';
|
|
72
|
+
default:
|
|
73
|
+
return 'unknown target';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getLatestAttemptFromTrace(trace) {
|
|
78
|
+
if (!trace || typeof trace !== 'object' || Array.isArray(trace)) return null;
|
|
79
|
+
const attempts = Array.isArray(trace.attempts) ? trace.attempts : [];
|
|
80
|
+
if (attempts.length === 0) return null;
|
|
81
|
+
const last = attempts[attempts.length - 1];
|
|
82
|
+
if (!last || typeof last !== 'object') return null;
|
|
83
|
+
const startedAt = typeof last.started_at === 'string' ? last.started_at : null;
|
|
84
|
+
const completedAt = typeof last.completed_at === 'string' ? last.completed_at : startedAt;
|
|
85
|
+
let latencyMs = null;
|
|
86
|
+
const startedMs = toMillis(startedAt);
|
|
87
|
+
const completedMs = toMillis(completedAt);
|
|
88
|
+
if (startedMs !== null && completedMs !== null && completedMs >= startedMs) {
|
|
89
|
+
latencyMs = completedMs - startedMs;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
at: completedAt,
|
|
93
|
+
turn_id: trace.turn_id || null,
|
|
94
|
+
runtime_id: trace.runtime_id || null,
|
|
95
|
+
attempts_made: Number.isInteger(trace.attempts_made) ? trace.attempts_made : attempts.length,
|
|
96
|
+
final_outcome: trace.final_outcome || null,
|
|
97
|
+
latency_ms: latencyMs,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildConnectorEntry(runtimeId, runtime) {
|
|
102
|
+
return {
|
|
103
|
+
runtime_id: runtimeId,
|
|
104
|
+
type: runtime.type,
|
|
105
|
+
target: formatTarget(runtime),
|
|
106
|
+
state: 'never_used',
|
|
107
|
+
reachable: 'unknown',
|
|
108
|
+
active_turn_ids: [],
|
|
109
|
+
active_roles: [],
|
|
110
|
+
last_turn_id: null,
|
|
111
|
+
last_role: null,
|
|
112
|
+
last_phase: null,
|
|
113
|
+
last_attempt_at: null,
|
|
114
|
+
last_success_at: null,
|
|
115
|
+
last_failure_at: null,
|
|
116
|
+
latency_ms: null,
|
|
117
|
+
attempts_made: null,
|
|
118
|
+
last_error: null,
|
|
119
|
+
_latest_success: null,
|
|
120
|
+
_latest_failure: null,
|
|
121
|
+
_latest_attempt: null,
|
|
122
|
+
_latest_identity: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applyIdentityTarget(entry, candidate) {
|
|
127
|
+
if (!candidate) return;
|
|
128
|
+
entry.last_turn_id = candidate.turn_id || entry.last_turn_id;
|
|
129
|
+
entry.last_role = candidate.role || entry.last_role;
|
|
130
|
+
entry.last_phase = candidate.phase || entry.last_phase;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finalizeConnectorEntry(entry) {
|
|
134
|
+
entry.last_attempt_at = entry._latest_attempt?.at || null;
|
|
135
|
+
entry.latency_ms = entry._latest_attempt?.latency_ms ?? null;
|
|
136
|
+
entry.attempts_made = entry._latest_attempt?.attempts_made ?? null;
|
|
137
|
+
entry.last_success_at = entry._latest_success?.at || null;
|
|
138
|
+
entry.last_failure_at = entry._latest_failure?.at || null;
|
|
139
|
+
entry.last_error = entry._latest_failure?.error || null;
|
|
140
|
+
|
|
141
|
+
const hasActive = entry.active_turn_ids.length > 0;
|
|
142
|
+
const hasFailure = Boolean(entry._latest_failure);
|
|
143
|
+
const hasSuccess = Boolean(entry._latest_success);
|
|
144
|
+
|
|
145
|
+
if (hasActive && !hasFailure) {
|
|
146
|
+
entry.state = 'active';
|
|
147
|
+
} else {
|
|
148
|
+
const failureMs = toMillis(entry.last_failure_at);
|
|
149
|
+
const successMs = toMillis(entry.last_success_at);
|
|
150
|
+
if (failureMs !== null && (successMs === null || failureMs >= successMs)) {
|
|
151
|
+
entry.state = 'failing';
|
|
152
|
+
} else if (hasSuccess || entry._latest_attempt?.final_outcome === 'success') {
|
|
153
|
+
entry.state = 'healthy';
|
|
154
|
+
} else if (hasActive) {
|
|
155
|
+
entry.state = 'active';
|
|
156
|
+
} else {
|
|
157
|
+
entry.state = 'never_used';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (entry.type === 'local_cli') {
|
|
162
|
+
entry.reachable = 'unknown';
|
|
163
|
+
} else if (entry._latest_attempt?.final_outcome === 'success' || entry.state === 'healthy') {
|
|
164
|
+
entry.reachable = 'yes';
|
|
165
|
+
} else if (entry._latest_failure) {
|
|
166
|
+
entry.reachable = 'no';
|
|
167
|
+
} else {
|
|
168
|
+
entry.reachable = 'unknown';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
delete entry._latest_success;
|
|
172
|
+
delete entry._latest_failure;
|
|
173
|
+
delete entry._latest_attempt;
|
|
174
|
+
delete entry._latest_identity;
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getConnectorHealth(root, config, state) {
|
|
179
|
+
const runtimeEntries = Object.entries(config?.runtimes || {})
|
|
180
|
+
.filter(([, runtime]) => CONNECTOR_RUNTIME_TYPES.has(runtime?.type))
|
|
181
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'));
|
|
182
|
+
|
|
183
|
+
const connectors = runtimeEntries.map(([runtimeId, runtime]) => buildConnectorEntry(runtimeId, runtime));
|
|
184
|
+
const connectorMap = Object.fromEntries(connectors.map((entry) => [entry.runtime_id, entry]));
|
|
185
|
+
|
|
186
|
+
const turnRuntimeIndex = {};
|
|
187
|
+
|
|
188
|
+
for (const turn of Object.values(state?.active_turns || {})) {
|
|
189
|
+
if (!turn || typeof turn !== 'object') continue;
|
|
190
|
+
const entry = connectorMap[turn.runtime_id];
|
|
191
|
+
if (!entry) continue;
|
|
192
|
+
if (typeof turn.turn_id === 'string') {
|
|
193
|
+
turnRuntimeIndex[turn.turn_id] = turn.runtime_id;
|
|
194
|
+
entry.active_turn_ids.push(turn.turn_id);
|
|
195
|
+
}
|
|
196
|
+
if (typeof turn.assigned_role === 'string' && !entry.active_roles.includes(turn.assigned_role)) {
|
|
197
|
+
entry.active_roles.push(turn.assigned_role);
|
|
198
|
+
}
|
|
199
|
+
entry._latest_identity = chooseLatestDefined(entry._latest_identity, {
|
|
200
|
+
turn_id: turn.turn_id || null,
|
|
201
|
+
role: turn.assigned_role || null,
|
|
202
|
+
phase: state?.phase || null,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const entry of connectors) {
|
|
207
|
+
entry.active_turn_ids.sort((a, b) => a.localeCompare(b, 'en'));
|
|
208
|
+
entry.active_roles.sort((a, b) => a.localeCompare(b, 'en'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const history = safeReadJsonl(join(root, HISTORY_PATH));
|
|
212
|
+
for (const item of history) {
|
|
213
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
214
|
+
const runtimeId = item.runtime_id;
|
|
215
|
+
const entry = connectorMap[runtimeId];
|
|
216
|
+
if (!entry) continue;
|
|
217
|
+
|
|
218
|
+
if (typeof item.turn_id === 'string') {
|
|
219
|
+
turnRuntimeIndex[item.turn_id] = runtimeId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const acceptedAt = typeof item.accepted_at === 'string' ? item.accepted_at : null;
|
|
223
|
+
if (!acceptedAt) continue;
|
|
224
|
+
|
|
225
|
+
const successCandidate = {
|
|
226
|
+
at: acceptedAt,
|
|
227
|
+
turn_id: item.turn_id || null,
|
|
228
|
+
role: item.role || null,
|
|
229
|
+
phase: item.phase || null,
|
|
230
|
+
};
|
|
231
|
+
entry._latest_success = chooseLatest(entry._latest_success, successCandidate);
|
|
232
|
+
entry._latest_identity = chooseLatestDefined(entry._latest_identity, successCandidate);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const stagingRoot = join(root, STAGING_DIR);
|
|
236
|
+
if (existsSync(stagingRoot)) {
|
|
237
|
+
for (const child of readdirSync(stagingRoot, { withFileTypes: true })) {
|
|
238
|
+
if (!child.isDirectory()) continue;
|
|
239
|
+
const turnId = child.name;
|
|
240
|
+
const turnDir = join(stagingRoot, turnId);
|
|
241
|
+
const trace = safeReadJson(join(turnDir, 'retry-trace.json'));
|
|
242
|
+
const apiError = safeReadJson(join(turnDir, 'api-error.json'));
|
|
243
|
+
const runtimeId = trace?.runtime_id || turnRuntimeIndex[turnId] || null;
|
|
244
|
+
const entry = runtimeId ? connectorMap[runtimeId] : null;
|
|
245
|
+
if (!entry) continue;
|
|
246
|
+
|
|
247
|
+
const latestAttempt = getLatestAttemptFromTrace(trace);
|
|
248
|
+
if (latestAttempt) {
|
|
249
|
+
entry._latest_attempt = chooseLatest(entry._latest_attempt, latestAttempt);
|
|
250
|
+
const identity = {
|
|
251
|
+
turn_id: latestAttempt.turn_id || turnId,
|
|
252
|
+
role: null,
|
|
253
|
+
phase: null,
|
|
254
|
+
};
|
|
255
|
+
if (latestAttempt.final_outcome === 'success') {
|
|
256
|
+
entry._latest_success = chooseLatest(entry._latest_success, {
|
|
257
|
+
at: latestAttempt.at,
|
|
258
|
+
turn_id: latestAttempt.turn_id || turnId,
|
|
259
|
+
role: null,
|
|
260
|
+
phase: null,
|
|
261
|
+
});
|
|
262
|
+
} else if (latestAttempt.final_outcome === 'failure' || latestAttempt.final_outcome === 'aborted') {
|
|
263
|
+
entry._latest_failure = chooseLatest(entry._latest_failure, {
|
|
264
|
+
at: latestAttempt.at,
|
|
265
|
+
turn_id: latestAttempt.turn_id || turnId,
|
|
266
|
+
role: null,
|
|
267
|
+
phase: null,
|
|
268
|
+
error: apiError?.message || latestAttempt.final_outcome,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
entry._latest_identity = chooseLatestDefined(entry._latest_identity, identity);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (apiError && !latestAttempt) {
|
|
275
|
+
const stats = statSync(turnDir);
|
|
276
|
+
entry._latest_failure = chooseLatest(entry._latest_failure, {
|
|
277
|
+
at: stats.mtime.toISOString(),
|
|
278
|
+
turn_id: turnId,
|
|
279
|
+
role: null,
|
|
280
|
+
phase: null,
|
|
281
|
+
error: apiError.message || apiError.error_class || 'runtime_error',
|
|
282
|
+
});
|
|
283
|
+
entry._latest_identity = chooseLatestDefined(entry._latest_identity, {
|
|
284
|
+
turn_id: turnId,
|
|
285
|
+
role: null,
|
|
286
|
+
phase: null,
|
|
287
|
+
});
|
|
288
|
+
} else if (apiError && entry._latest_failure && entry._latest_failure.turn_id === turnId) {
|
|
289
|
+
entry._latest_failure.error = apiError.message || apiError.error_class || entry._latest_failure.error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const blockedTurnId = state?.blocked_reason?.turn_id || null;
|
|
295
|
+
if (blockedTurnId && turnRuntimeIndex[blockedTurnId] && connectorMap[turnRuntimeIndex[blockedTurnId]]) {
|
|
296
|
+
const entry = connectorMap[turnRuntimeIndex[blockedTurnId]];
|
|
297
|
+
const detail = state?.blocked_reason?.recovery?.detail || state?.blocked_on || 'runtime_blocked';
|
|
298
|
+
const blockedAt = state?.blocked_reason?.blocked_at || null;
|
|
299
|
+
entry._latest_failure = chooseLatest(entry._latest_failure, {
|
|
300
|
+
at: blockedAt,
|
|
301
|
+
turn_id: blockedTurnId,
|
|
302
|
+
role: null,
|
|
303
|
+
phase: state?.phase || null,
|
|
304
|
+
error: detail,
|
|
305
|
+
});
|
|
306
|
+
entry._latest_identity = chooseLatestDefined(entry._latest_identity, {
|
|
307
|
+
turn_id: blockedTurnId,
|
|
308
|
+
role: null,
|
|
309
|
+
phase: state?.phase || null,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
connectors: connectors.map((entry) => {
|
|
315
|
+
applyIdentityTarget(entry, entry._latest_failure || entry._latest_success || entry._latest_identity);
|
|
316
|
+
return finalizeConnectorEntry(entry);
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -19,6 +19,7 @@ import { FileWatcher } from './file-watcher.js';
|
|
|
19
19
|
import { approvePendingDashboardGate } from './actions.js';
|
|
20
20
|
import { readCoordinatorBlockerSnapshot } from './coordinator-blockers.js';
|
|
21
21
|
import { readWorkflowKitArtifacts } from './workflow-kit-artifacts.js';
|
|
22
|
+
import { readConnectorHealthSnapshot } from './connectors.js';
|
|
22
23
|
|
|
23
24
|
const MIME_TYPES = {
|
|
24
25
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -286,6 +287,12 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847 }
|
|
|
286
287
|
return;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
if (pathname === '/api/connectors') {
|
|
291
|
+
const result = readConnectorHealthSnapshot(workspacePath);
|
|
292
|
+
writeJson(res, result.status, result.body);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
289
296
|
// API routes
|
|
290
297
|
if (pathname.startsWith('/api/')) {
|
|
291
298
|
const result = readResource(agentxchainDir, pathname);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadProjectContext } from '../config.js';
|
|
2
|
+
import { readJsonFile } from './state-reader.js';
|
|
3
|
+
import { getConnectorHealth } from '../connector-health.js';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
export function readConnectorHealthSnapshot(workspacePath) {
|
|
7
|
+
const context = loadProjectContext(workspacePath);
|
|
8
|
+
if (!context || context.config.protocol_mode !== 'governed') {
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
status: 404,
|
|
12
|
+
body: {
|
|
13
|
+
ok: false,
|
|
14
|
+
code: 'config_missing',
|
|
15
|
+
error: 'Project config not found. Run `agentxchain init --governed` first.',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const state = readJsonFile(join(workspacePath, '.agentxchain'), 'state.json');
|
|
21
|
+
if (!state) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 404,
|
|
25
|
+
body: {
|
|
26
|
+
ok: false,
|
|
27
|
+
code: 'state_missing',
|
|
28
|
+
error: 'Run state not found. Run `agentxchain init --governed` first.',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
status: 200,
|
|
36
|
+
body: {
|
|
37
|
+
ok: true,
|
|
38
|
+
...getConnectorHealth(workspacePath, context.config, state),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* See: WORKFLOW_KIT_DASHBOARD_SPEC.md
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync } from 'fs';
|
|
14
13
|
import { join } from 'path';
|
|
15
|
-
import { loadConfig } from '../config.js';
|
|
14
|
+
import { loadConfig, loadProjectContext, loadProjectState } from '../config.js';
|
|
15
|
+
import { deriveWorkflowKitArtifacts } from '../workflow-kit-artifacts.js';
|
|
16
16
|
import { readJsonFile } from './state-reader.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -22,8 +22,10 @@ import { readJsonFile } from './state-reader.js';
|
|
|
22
22
|
* @returns {{ ok: boolean, status: number, body: object }}
|
|
23
23
|
*/
|
|
24
24
|
export function readWorkflowKitArtifacts(workspacePath) {
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const context = loadProjectContext(workspacePath);
|
|
26
|
+
const governedContext = context?.config?.protocol_mode === 'governed' ? context : null;
|
|
27
|
+
const legacyConfigResult = governedContext ? null : loadConfig(workspacePath);
|
|
28
|
+
if (!governedContext && !legacyConfigResult) {
|
|
27
29
|
return {
|
|
28
30
|
ok: false,
|
|
29
31
|
status: 404,
|
|
@@ -35,8 +37,11 @@ export function readWorkflowKitArtifacts(workspacePath) {
|
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
+
const root = governedContext?.root || legacyConfigResult.root;
|
|
41
|
+
const config = governedContext?.config || legacyConfigResult.config;
|
|
42
|
+
const state = governedContext
|
|
43
|
+
? loadProjectState(root, config)
|
|
44
|
+
: readJsonFile(join(root, '.agentxchain'), 'state.json');
|
|
40
45
|
if (!state) {
|
|
41
46
|
return {
|
|
42
47
|
ok: false,
|
|
@@ -49,7 +54,6 @@ export function readWorkflowKitArtifacts(workspacePath) {
|
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
const config = configResult.config;
|
|
53
57
|
const phase = state.phase || null;
|
|
54
58
|
|
|
55
59
|
if (!config.workflow_kit) {
|
|
@@ -89,8 +93,8 @@ export function readWorkflowKitArtifacts(workspacePath) {
|
|
|
89
93
|
};
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
96
|
+
const snapshot = deriveWorkflowKitArtifacts(root, config, state);
|
|
97
|
+
if (!snapshot) {
|
|
94
98
|
return {
|
|
95
99
|
ok: true,
|
|
96
100
|
status: 200,
|
|
@@ -102,30 +106,13 @@ export function readWorkflowKitArtifacts(workspacePath) {
|
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
const entryRole = config.routing?.[phase]?.entry_role || null;
|
|
106
|
-
|
|
107
|
-
const result = artifacts
|
|
108
|
-
.filter((a) => a && typeof a.path === 'string')
|
|
109
|
-
.map((a) => {
|
|
110
|
-
const hasExplicitOwner = typeof a.owned_by === 'string' && a.owned_by.length > 0;
|
|
111
|
-
return {
|
|
112
|
-
path: a.path,
|
|
113
|
-
required: a.required !== false,
|
|
114
|
-
semantics: a.semantics || null,
|
|
115
|
-
owned_by: hasExplicitOwner ? a.owned_by : entryRole,
|
|
116
|
-
owner_resolution: hasExplicitOwner ? 'explicit' : 'entry_role',
|
|
117
|
-
exists: existsSync(join(workspacePath, a.path)),
|
|
118
|
-
};
|
|
119
|
-
})
|
|
120
|
-
.sort((a, b) => a.path.localeCompare(b.path, 'en'));
|
|
121
|
-
|
|
122
109
|
return {
|
|
123
110
|
ok: true,
|
|
124
111
|
status: 200,
|
|
125
112
|
body: {
|
|
126
113
|
ok: true,
|
|
127
114
|
phase,
|
|
128
|
-
artifacts:
|
|
115
|
+
artifacts: snapshot.artifacts,
|
|
129
116
|
},
|
|
130
117
|
};
|
|
131
118
|
}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
import { validateHooksConfig } from './hook-runner.js';
|
|
16
16
|
import { validateNotificationsConfig } from './notification-runner.js';
|
|
17
17
|
import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
18
|
+
import {
|
|
19
|
+
buildDefaultWorkflowKitArtifactsForPhase,
|
|
20
|
+
expandWorkflowKitPhaseArtifacts,
|
|
21
|
+
isWorkflowKitPhaseTemplateId,
|
|
22
|
+
VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS,
|
|
23
|
+
} from './workflow-kit-phase-templates.js';
|
|
18
24
|
|
|
19
25
|
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
20
26
|
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp', 'remote_agent'];
|
|
@@ -26,25 +32,6 @@ export { DEFAULT_PHASES };
|
|
|
26
32
|
const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
27
33
|
const VALID_SEMANTIC_IDS = ['pm_signoff', 'system_spec', 'implementation_notes', 'acceptance_matrix', 'ship_verdict', 'release_notes', 'section_check'];
|
|
28
34
|
|
|
29
|
-
/**
|
|
30
|
-
* Default artifact map for phases when workflow_kit is absent from config.
|
|
31
|
-
* Only phases present in this map get default artifacts.
|
|
32
|
-
*/
|
|
33
|
-
const DEFAULT_PHASE_ARTIFACTS = {
|
|
34
|
-
planning: [
|
|
35
|
-
{ path: '.planning/PM_SIGNOFF.md', semantics: 'pm_signoff', required: true },
|
|
36
|
-
{ path: '.planning/SYSTEM_SPEC.md', semantics: 'system_spec', required: true },
|
|
37
|
-
{ path: '.planning/ROADMAP.md', semantics: null, required: true },
|
|
38
|
-
],
|
|
39
|
-
implementation: [
|
|
40
|
-
{ path: '.planning/IMPLEMENTATION_NOTES.md', semantics: 'implementation_notes', required: true },
|
|
41
|
-
],
|
|
42
|
-
qa: [
|
|
43
|
-
{ path: '.planning/acceptance-matrix.md', semantics: 'acceptance_matrix', required: true },
|
|
44
|
-
{ path: '.planning/ship-verdict.md', semantics: 'ship_verdict', required: true },
|
|
45
|
-
{ path: '.planning/RELEASE_NOTES.md', semantics: 'release_notes', required: true },
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
35
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
49
36
|
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
50
37
|
'rate_limited',
|
|
@@ -566,14 +553,52 @@ export function validateWorkflowKitConfig(wk, routing, roles) {
|
|
|
566
553
|
continue;
|
|
567
554
|
}
|
|
568
555
|
|
|
569
|
-
|
|
556
|
+
let templateValid = true;
|
|
557
|
+
if (phaseConfig.template !== undefined) {
|
|
558
|
+
if (typeof phaseConfig.template !== 'string' || !phaseConfig.template.trim()) {
|
|
559
|
+
errors.push(`workflow_kit.phases.${phase}.template must be a non-empty string`);
|
|
560
|
+
templateValid = false;
|
|
561
|
+
} else if (!isWorkflowKitPhaseTemplateId(phaseConfig.template)) {
|
|
562
|
+
errors.push(
|
|
563
|
+
`workflow_kit.phases.${phase}.template "${phaseConfig.template}" is unknown; valid values: ${VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.join(', ')}`,
|
|
564
|
+
);
|
|
565
|
+
templateValid = false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (phaseConfig.artifacts !== undefined && !Array.isArray(phaseConfig.artifacts)) {
|
|
570
570
|
errors.push(`workflow_kit.phases.${phase}.artifacts must be an array`);
|
|
571
571
|
continue;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
if (Array.isArray(phaseConfig.artifacts)) {
|
|
575
|
+
const explicitSeenPaths = new Set();
|
|
576
|
+
for (const artifact of phaseConfig.artifacts) {
|
|
577
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (typeof artifact.path !== 'string' || !artifact.path.trim()) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (explicitSeenPaths.has(artifact.path)) {
|
|
584
|
+
errors.push(`duplicate artifact path "${artifact.path}" in phase "${phase}"`);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
explicitSeenPaths.add(artifact.path);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (phaseConfig.template === undefined && phaseConfig.artifacts === undefined) {
|
|
592
|
+
errors.push(`workflow_kit.phases.${phase} must declare template, artifacts, or both`);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
574
596
|
const seenPaths = new Set();
|
|
575
|
-
|
|
576
|
-
|
|
597
|
+
const expandedArtifacts = templateValid
|
|
598
|
+
? expandWorkflowKitPhaseArtifacts(phaseConfig)
|
|
599
|
+
: Array.isArray(phaseConfig.artifacts) ? phaseConfig.artifacts : [];
|
|
600
|
+
for (let i = 0; i < expandedArtifacts.length; i++) {
|
|
601
|
+
const artifact = expandedArtifacts[i];
|
|
577
602
|
const prefix = `workflow_kit.phases.${phase}.artifacts[${i}]`;
|
|
578
603
|
|
|
579
604
|
if (!artifact || typeof artifact !== 'object') {
|
|
@@ -845,7 +870,7 @@ export function getMaxConcurrentTurns(config, phase) {
|
|
|
845
870
|
|
|
846
871
|
/**
|
|
847
872
|
* Normalize workflow_kit config.
|
|
848
|
-
* When absent, builds defaults from routing phases using
|
|
873
|
+
* When absent, builds defaults from routing phases using the built-in phase templates.
|
|
849
874
|
* When present, normalizes artifact entries.
|
|
850
875
|
*/
|
|
851
876
|
export function normalizeWorkflowKit(raw, routingPhases) {
|
|
@@ -862,7 +887,7 @@ export function normalizeWorkflowKit(raw, routingPhases) {
|
|
|
862
887
|
if (raw.phases) {
|
|
863
888
|
for (const [phase, phaseConfig] of Object.entries(raw.phases)) {
|
|
864
889
|
phases[phase] = {
|
|
865
|
-
artifacts: (phaseConfig
|
|
890
|
+
artifacts: expandWorkflowKitPhaseArtifacts(phaseConfig).map(a => ({
|
|
866
891
|
path: a.path,
|
|
867
892
|
semantics: a.semantics || null,
|
|
868
893
|
semantics_config: a.semantics_config || null,
|
|
@@ -879,9 +904,10 @@ export function normalizeWorkflowKit(raw, routingPhases) {
|
|
|
879
904
|
function buildDefaultWorkflowKit(routingPhases) {
|
|
880
905
|
const phases = {};
|
|
881
906
|
for (const phase of routingPhases) {
|
|
882
|
-
|
|
907
|
+
const templateArtifacts = buildDefaultWorkflowKitArtifactsForPhase(phase);
|
|
908
|
+
if (templateArtifacts) {
|
|
883
909
|
phases[phase] = {
|
|
884
|
-
artifacts:
|
|
910
|
+
artifacts: templateArtifacts.map(a => ({ ...a, semantics_config: a.semantics_config || null })),
|
|
885
911
|
};
|
|
886
912
|
}
|
|
887
913
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export function deriveWorkflowKitArtifacts(root, config, state) {
|
|
5
|
+
if (!config?.workflow_kit) return null;
|
|
6
|
+
|
|
7
|
+
const phase = state?.phase || null;
|
|
8
|
+
if (!phase) return null;
|
|
9
|
+
|
|
10
|
+
const phaseConfig = config.workflow_kit.phases?.[phase];
|
|
11
|
+
if (!phaseConfig) return null;
|
|
12
|
+
|
|
13
|
+
const artifacts = Array.isArray(phaseConfig.artifacts) ? phaseConfig.artifacts : [];
|
|
14
|
+
if (artifacts.length === 0) return null;
|
|
15
|
+
|
|
16
|
+
const entryRole = config.routing?.[phase]?.entry_role || null;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
phase,
|
|
21
|
+
artifacts: artifacts
|
|
22
|
+
.filter((artifact) => artifact && typeof artifact.path === 'string')
|
|
23
|
+
.map((artifact) => {
|
|
24
|
+
const hasExplicitOwner = typeof artifact.owned_by === 'string' && artifact.owned_by.length > 0;
|
|
25
|
+
return {
|
|
26
|
+
path: artifact.path,
|
|
27
|
+
required: artifact.required !== false,
|
|
28
|
+
semantics: artifact.semantics || null,
|
|
29
|
+
owned_by: hasExplicitOwner ? artifact.owned_by : entryRole,
|
|
30
|
+
owner_resolution: hasExplicitOwner ? 'explicit' : 'entry_role',
|
|
31
|
+
exists: existsSync(join(root, artifact.path)),
|
|
32
|
+
};
|
|
33
|
+
})
|
|
34
|
+
.sort((a, b) => a.path.localeCompare(b.path, 'en')),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
function cloneJsonCompatible(value) {
|
|
2
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY = Object.freeze({
|
|
6
|
+
'planning-default': {
|
|
7
|
+
description: 'Core planning proof surface for governed repos.',
|
|
8
|
+
artifacts: [
|
|
9
|
+
{ path: '.planning/PM_SIGNOFF.md', semantics: 'pm_signoff', required: true },
|
|
10
|
+
{ path: '.planning/SYSTEM_SPEC.md', semantics: 'system_spec', required: true },
|
|
11
|
+
{ path: '.planning/ROADMAP.md', semantics: null, required: true },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
'implementation-default': {
|
|
15
|
+
description: 'Implementation proof surface for governed repos.',
|
|
16
|
+
artifacts: [
|
|
17
|
+
{ path: '.planning/IMPLEMENTATION_NOTES.md', semantics: 'implementation_notes', required: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
'qa-default': {
|
|
21
|
+
description: 'QA and ship-verdict proof surface for governed repos.',
|
|
22
|
+
artifacts: [
|
|
23
|
+
{ path: '.planning/acceptance-matrix.md', semantics: 'acceptance_matrix', required: true },
|
|
24
|
+
{ path: '.planning/ship-verdict.md', semantics: 'ship_verdict', required: true },
|
|
25
|
+
{ path: '.planning/RELEASE_NOTES.md', semantics: 'release_notes', required: true },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
'architecture-review': {
|
|
29
|
+
description: 'Structured architecture-review document with required sections.',
|
|
30
|
+
artifacts: [
|
|
31
|
+
{
|
|
32
|
+
path: '.planning/ARCHITECTURE.md',
|
|
33
|
+
semantics: 'section_check',
|
|
34
|
+
semantics_config: {
|
|
35
|
+
required_sections: ['## Context', '## Proposed Design', '## Trade-offs', '## Risks'],
|
|
36
|
+
},
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
'security-review': {
|
|
42
|
+
description: 'Structured security-review document with required sections.',
|
|
43
|
+
artifacts: [
|
|
44
|
+
{
|
|
45
|
+
path: '.planning/SECURITY_REVIEW.md',
|
|
46
|
+
semantics: 'section_check',
|
|
47
|
+
semantics_config: {
|
|
48
|
+
required_sections: ['## Threat Model', '## Findings', '## Verdict'],
|
|
49
|
+
},
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS = Object.freeze(
|
|
57
|
+
Object.keys(WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const DEFAULT_WORKFLOW_KIT_PHASE_TEMPLATE_BY_PHASE = Object.freeze({
|
|
61
|
+
planning: 'planning-default',
|
|
62
|
+
implementation: 'implementation-default',
|
|
63
|
+
qa: 'qa-default',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export function listWorkflowKitPhaseTemplates() {
|
|
67
|
+
return VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.map((id) => loadWorkflowKitPhaseTemplate(id));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isWorkflowKitPhaseTemplateId(templateId) {
|
|
71
|
+
return VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.includes(templateId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadWorkflowKitPhaseTemplate(templateId) {
|
|
75
|
+
if (!isWorkflowKitPhaseTemplateId(templateId)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Unknown workflow-kit phase template "${templateId}". Valid templates: ${VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.join(', ')}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const template = WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY[templateId];
|
|
82
|
+
return {
|
|
83
|
+
id: templateId,
|
|
84
|
+
description: template.description,
|
|
85
|
+
artifacts: cloneJsonCompatible(template.artifacts),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function expandWorkflowKitPhaseArtifacts(phaseConfig = {}) {
|
|
90
|
+
const templateArtifacts = phaseConfig.template
|
|
91
|
+
? loadWorkflowKitPhaseTemplate(phaseConfig.template).artifacts
|
|
92
|
+
: [];
|
|
93
|
+
const explicitArtifacts = Array.isArray(phaseConfig.artifacts)
|
|
94
|
+
? cloneJsonCompatible(phaseConfig.artifacts)
|
|
95
|
+
: [];
|
|
96
|
+
const mergedArtifacts = templateArtifacts.map((artifact) => cloneJsonCompatible(artifact));
|
|
97
|
+
const indexByPath = new Map(
|
|
98
|
+
mergedArtifacts
|
|
99
|
+
.filter((artifact) => artifact && typeof artifact.path === 'string')
|
|
100
|
+
.map((artifact, index) => [artifact.path, index]),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
for (const artifact of explicitArtifacts) {
|
|
104
|
+
if (!artifact || typeof artifact.path !== 'string') {
|
|
105
|
+
mergedArtifacts.push(artifact);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const existingIndex = indexByPath.get(artifact.path);
|
|
110
|
+
if (existingIndex === undefined) {
|
|
111
|
+
indexByPath.set(artifact.path, mergedArtifacts.length);
|
|
112
|
+
mergedArtifacts.push(artifact);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mergedArtifacts[existingIndex] = {
|
|
117
|
+
...mergedArtifacts[existingIndex],
|
|
118
|
+
...artifact,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return mergedArtifacts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildDefaultWorkflowKitArtifactsForPhase(phase) {
|
|
126
|
+
const templateId = DEFAULT_WORKFLOW_KIT_PHASE_TEMPLATE_BY_PHASE[phase];
|
|
127
|
+
return templateId ? loadWorkflowKitPhaseTemplate(templateId).artifacts : null;
|
|
128
|
+
}
|
|
@@ -145,49 +145,33 @@
|
|
|
145
145
|
"workflow_kit": {
|
|
146
146
|
"phases": {
|
|
147
147
|
"planning": {
|
|
148
|
-
"
|
|
149
|
-
{ "path": ".planning/PM_SIGNOFF.md", "semantics": "pm_signoff", "required": true },
|
|
150
|
-
{ "path": ".planning/SYSTEM_SPEC.md", "semantics": "system_spec", "required": true },
|
|
151
|
-
{ "path": ".planning/ROADMAP.md", "semantics": null, "required": true }
|
|
152
|
-
]
|
|
148
|
+
"template": "planning-default"
|
|
153
149
|
},
|
|
154
150
|
"architecture": {
|
|
151
|
+
"template": "architecture-review",
|
|
155
152
|
"artifacts": [
|
|
156
153
|
{
|
|
157
154
|
"path": ".planning/ARCHITECTURE.md",
|
|
158
|
-
"semantics": "section_check",
|
|
159
155
|
"owned_by": "architect",
|
|
160
|
-
"semantics_config": {
|
|
161
|
-
"required_sections": ["## Context", "## Proposed Design", "## Trade-offs", "## Risks"]
|
|
162
|
-
},
|
|
163
156
|
"required": true
|
|
164
157
|
}
|
|
165
158
|
]
|
|
166
159
|
},
|
|
167
160
|
"implementation": {
|
|
168
|
-
"
|
|
169
|
-
{ "path": ".planning/IMPLEMENTATION_NOTES.md", "semantics": "implementation_notes", "required": true }
|
|
170
|
-
]
|
|
161
|
+
"template": "implementation-default"
|
|
171
162
|
},
|
|
172
163
|
"security_review": {
|
|
164
|
+
"template": "security-review",
|
|
173
165
|
"artifacts": [
|
|
174
166
|
{
|
|
175
167
|
"path": ".planning/SECURITY_REVIEW.md",
|
|
176
|
-
"semantics": "section_check",
|
|
177
168
|
"owned_by": "security_reviewer",
|
|
178
|
-
"semantics_config": {
|
|
179
|
-
"required_sections": ["## Threat Model", "## Findings", "## Verdict"]
|
|
180
|
-
},
|
|
181
169
|
"required": true
|
|
182
170
|
}
|
|
183
171
|
]
|
|
184
172
|
},
|
|
185
173
|
"qa": {
|
|
186
|
-
"
|
|
187
|
-
{ "path": ".planning/acceptance-matrix.md", "semantics": "acceptance_matrix", "required": true },
|
|
188
|
-
{ "path": ".planning/ship-verdict.md", "semantics": "ship_verdict", "required": true },
|
|
189
|
-
{ "path": ".planning/RELEASE_NOTES.md", "semantics": "release_notes", "required": true }
|
|
190
|
-
]
|
|
174
|
+
"template": "qa-default"
|
|
191
175
|
}
|
|
192
176
|
}
|
|
193
177
|
}
|