@magnusekdahl/parallix 1.0.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/CHANGELOG.md +140 -0
- package/LICENSE +661 -0
- package/README.md +196 -0
- package/config/agents.json +25 -0
- package/config/agents.local.json.template +8 -0
- package/config/state-map.json +4 -0
- package/config/state-map.json.template +31 -0
- package/config/workflow.config.schema.json +98 -0
- package/data/.gitkeep +0 -0
- package/docs/adr/0031-ai-agent-instruction-boundary-and-command-floor.md +114 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +135 -0
- package/docs/adr/0034-module-and-skill-invocation-model.md +202 -0
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +79 -0
- package/docs/adr/0037-ai-workflow-coordination-architecture.md +162 -0
- package/docs/adr/0041-integration-pipeline-gates.md +165 -0
- package/docs/adr/0042-workflow-cli-color-rendering-approach.md +106 -0
- package/docs/adr/0043-git-target-resolution-strategy.md +185 -0
- package/docs/adr/0044-workflow-distribution-model.md +277 -0
- package/docs/adr/0045-parallax-branch-model.md +182 -0
- package/docs/adr/0046-npm-publish-process-and-security.md +138 -0
- package/docs/adr/index.md +20 -0
- package/docs/agents.md +212 -0
- package/docs/authority-reference.md +298 -0
- package/docs/forgejo-setup.md +31 -0
- package/docs/migration/extraction.md +61 -0
- package/docs/migration/task-classification.md +36 -0
- package/docs/operator-setup.md +76 -0
- package/docs/readme-rewrite-benchmark.md +188 -0
- package/docs/use-cases.md +105 -0
- package/examples/README.md +62 -0
- package/examples/run-enterprise-tarball-workflow-smoke.sh +257 -0
- package/examples/run-verify-env-smoke.sh +40 -0
- package/index.js +250 -0
- package/lib/README.md +13 -0
- package/lib/agents/agents.js +867 -0
- package/lib/agents/claude-telemetry.js +233 -0
- package/lib/agents/claude.js +139 -0
- package/lib/agents/codex-telemetry.js +202 -0
- package/lib/agents/codex.js +219 -0
- package/lib/agents/limit-hit.js +252 -0
- package/lib/agents/mistral-telemetry.js +44 -0
- package/lib/agents/mistral.js +68 -0
- package/lib/agents/opencode-export.js +110 -0
- package/lib/agents/opencode-telemetry.js +356 -0
- package/lib/agents/opencode.js +218 -0
- package/lib/agents/stage-telemetry.js +37 -0
- package/lib/commands/active.js +625 -0
- package/lib/commands/checkpoint.js +76 -0
- package/lib/commands/config.js +39 -0
- package/lib/commands/coverage-gate.js +358 -0
- package/lib/commands/diff.js +119 -0
- package/lib/commands/draft.js +854 -0
- package/lib/commands/handoff.js +501 -0
- package/lib/commands/integrate.js +1528 -0
- package/lib/commands/mission-start.js +246 -0
- package/lib/commands/rebase.js +597 -0
- package/lib/commands/repair-handoff.js +227 -0
- package/lib/commands/resolve-conflict.js +109 -0
- package/lib/commands/review.js +13 -0
- package/lib/commands/setup-review.js +13 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/stats-backfill.js +395 -0
- package/lib/commands/stats.js +1601 -0
- package/lib/commands/status.js +183 -0
- package/lib/commands/verify.js +1 -0
- package/lib/core/fmt.js +202 -0
- package/lib/core/git.js +73 -0
- package/lib/core/gitignore.js +110 -0
- package/lib/core/mission-utils.js +1017 -0
- package/lib/core/persistent-data-migration.js +201 -0
- package/lib/core/product-config.js +508 -0
- package/lib/core/runtime-matrix.js +82 -0
- package/lib/core/spawn-tee.js +173 -0
- package/lib/core/state-map.js +89 -0
- package/lib/core/storage.js +165 -0
- package/lib/core/verification.js +149 -0
- package/lib/index.js +77 -0
- package/lib/review/rebase.js +163 -0
- package/lib/review/review-adapter.js +135 -0
- package/lib/review/review-artifacts.js +619 -0
- package/lib/review/review-commands.js +1375 -0
- package/lib/review/review-events.js +1007 -0
- package/lib/review/review-loop.js +1004 -0
- package/lib/review/review-polling.js +141 -0
- package/lib/review/review-prompts.js +212 -0
- package/lib/review/review-state.js +280 -0
- package/lib/review/review.js +96 -0
- package/lib/tools/backlog.js +680 -0
- package/lib/tools/forgejo.js +1585 -0
- package/lib/tools/gatekeeper.js +106 -0
- package/lib/tools/sessions.js +74 -0
- package/lib/tools/setup-review.js +1053 -0
- package/package.json +56 -0
- package/prompts/act-on-review-verbose.md +20 -0
- package/prompts/act-on-review.md +22 -0
- package/prompts/draft.md +20 -0
- package/prompts/execute.md +24 -0
- package/prompts/portfolio.md +30 -0
- package/prompts/review-verbose.md +20 -0
- package/prompts/review.md +17 -0
- package/px.js +236 -0
- package/templates/AGENTS-snippet.md +14 -0
- package/templates/AGENTS.md.template +34 -0
- package/templates/CLAUDE.md.template +27 -0
- package/templates/CODEX.md.template +38 -0
- package/templates/MISTRAL.md.template +24 -0
- package/templates/claude-commands/act-on-review.md +3 -0
- package/templates/claude-commands/area-review.md +3 -0
- package/templates/claude-commands/draft.md +6 -0
- package/templates/claude-commands/execute.md +6 -0
- package/templates/claude-commands/integrate.md +4 -0
- package/templates/claude-commands/portfolio.md +5 -0
- package/templates/claude-commands/review.md +4 -0
- package/templates/codex/config.toml +6 -0
- package/templates/mission-scaffold.md +39 -0
- package/templates/vibe/skills/act-on-review/SKILL.md +16 -0
- package/templates/vibe/skills/area-review/SKILL.md +16 -0
- package/templates/vibe/skills/draft/SKILL.md +16 -0
- package/templates/vibe/skills/execute/SKILL.md +16 -0
- package/templates/vibe/skills/integrate/SKILL.md +16 -0
- package/templates/vibe/skills/portfolio/SKILL.md +21 -0
- package/templates/vibe/skills/review/SKILL.md +16 -0
- package/tools/setup-forgejo-docker.sh +84 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-verify-env-smoke.sh — prove the provisional px runner from inside a
|
|
3
|
+
# caller-supplied temporary target repo. No fixed workstation paths.
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./run-verify-env-smoke.sh [parallix-workflow-dir]
|
|
6
|
+
# parallix-workflow-dir defaults to the directory containing this example's
|
|
7
|
+
# px.js (one level up from examples/).
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
EXAMPLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
PARALLIX_DIR="$(cd "${1:-${EXAMPLES_DIR}/..}" && pwd)"
|
|
13
|
+
PX="${PARALLIX_DIR}/px.js"
|
|
14
|
+
|
|
15
|
+
if [[ ! -f "${PX}" ]]; then
|
|
16
|
+
echo "FAIL: px.js not found at ${PX}" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Caller-supplied target repo: a throwaway directory, never a fixed OS path.
|
|
21
|
+
TARGET_REPO="$(mktemp -d "${TMPDIR:-/tmp}/parallix-example-target.XXXXXX")"
|
|
22
|
+
cleanup() { rm -rf "${TARGET_REPO}"; }
|
|
23
|
+
trap cleanup EXIT
|
|
24
|
+
|
|
25
|
+
echo "parallix dir: ${PARALLIX_DIR}"
|
|
26
|
+
echo "Target repo : ${TARGET_REPO}"
|
|
27
|
+
echo "Running from target repo: node px.js verify-env"
|
|
28
|
+
|
|
29
|
+
(
|
|
30
|
+
cd "${TARGET_REPO}"
|
|
31
|
+
node "${PX}" verify-env
|
|
32
|
+
)
|
|
33
|
+
STATUS=$?
|
|
34
|
+
|
|
35
|
+
if [[ "${STATUS}" -eq 0 ]]; then
|
|
36
|
+
echo "PASS: verify-env exited 0 against the temporary target repo"
|
|
37
|
+
else
|
|
38
|
+
echo "FAIL: verify-env exited ${STATUS}" >&2
|
|
39
|
+
fi
|
|
40
|
+
exit "${STATUS}"
|
package/index.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VisualBoard Parallix Coordination CLI
|
|
5
|
+
* ADR 0037 — Wave 1
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fmt = require('./lib/core/fmt');
|
|
11
|
+
const { ensureStandaloneGitRepo } = require('./lib/core/product-config');
|
|
12
|
+
const { loadStateMap } = require('./lib/core/state-map');
|
|
13
|
+
|
|
14
|
+
// Fixed virtual-state → canonical-command invariants for alias derivation.
|
|
15
|
+
const STATE_COMMAND_MAP = {
|
|
16
|
+
ready: 'draft',
|
|
17
|
+
approved: 'integrate',
|
|
18
|
+
done: 'integrate',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const KNOWN_COMMANDS = [
|
|
22
|
+
'mission-start',
|
|
23
|
+
'verify-env',
|
|
24
|
+
'verify',
|
|
25
|
+
'setup',
|
|
26
|
+
'setup-review',
|
|
27
|
+
'draft',
|
|
28
|
+
'active',
|
|
29
|
+
'status',
|
|
30
|
+
'checkpoint',
|
|
31
|
+
'review',
|
|
32
|
+
'handoff',
|
|
33
|
+
'integrate',
|
|
34
|
+
'resolve-conflict',
|
|
35
|
+
'rebase',
|
|
36
|
+
'stats',
|
|
37
|
+
'aliases',
|
|
38
|
+
'config',
|
|
39
|
+
'diff',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const READ_ONLY_COMMANDS = new Set(['config']);
|
|
43
|
+
|
|
44
|
+
function loadStateMapForAliases(options = {}) {
|
|
45
|
+
return loadStateMap(options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Derives the command-alias table from state-map.json and fixed parallix invariants.
|
|
49
|
+
// Users only maintain state-map.json; aliases update automatically.
|
|
50
|
+
function deriveAliases(options = {}) {
|
|
51
|
+
const stateMap = loadStateMapForAliases(options);
|
|
52
|
+
const aliases = { ...STATE_COMMAND_MAP };
|
|
53
|
+
for (const [virtual, actual] of Object.entries(stateMap)) {
|
|
54
|
+
if (actual && actual !== virtual && STATE_COMMAND_MAP[virtual]) {
|
|
55
|
+
aliases[actual] = STATE_COMMAND_MAP[virtual];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return aliases;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveAlias(command, aliases = deriveAliases()) {
|
|
62
|
+
return Object.prototype.hasOwnProperty.call(aliases, command) ? aliases[command] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printAliases(aliases, logFn = fmt.log.plain) {
|
|
66
|
+
const entries = Object.entries(aliases).sort(([a], [b]) => a.localeCompare(b));
|
|
67
|
+
if (entries.length === 0) {
|
|
68
|
+
logFn('No aliases configured.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
logFn(fmt.bold('alias canonical'));
|
|
72
|
+
logFn(fmt.bold('--- ---'));
|
|
73
|
+
for (const [alias, canonical] of entries) {
|
|
74
|
+
logFn(`${alias.padEnd(30)}${canonical}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main(args = process.argv.slice(2), {
|
|
79
|
+
existsSyncFn = fs.existsSync,
|
|
80
|
+
cwdFn = () => process.cwd(),
|
|
81
|
+
ensureStandaloneGitRepoFn = ensureStandaloneGitRepo,
|
|
82
|
+
requireFn = require,
|
|
83
|
+
printUsageFn = printUsage,
|
|
84
|
+
exitFn = process.exit,
|
|
85
|
+
errorFn = fmt.log.plainError,
|
|
86
|
+
logFn = fmt.log.plain,
|
|
87
|
+
loadAliasesFn = deriveAliases,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const command = args[0];
|
|
90
|
+
|
|
91
|
+
if (command === '--version' || command === '-v') {
|
|
92
|
+
const pkg = requireFn(path.join(__dirname, 'package.json'));
|
|
93
|
+
logFn(pkg.name + '@' + pkg.version);
|
|
94
|
+
exitFn(0);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
99
|
+
printUsageFn();
|
|
100
|
+
exitFn(0);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === 'aliases') {
|
|
105
|
+
printAliases(loadAliasesFn({ rootDir: cwdFn() }), logFn);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const libPath = path.join(__dirname, 'lib', 'commands', `${command}.js`);
|
|
110
|
+
|
|
111
|
+
// Special case: verify-env is an alias for mission-start diagnostic.
|
|
112
|
+
// active is now its own command (real agent-start path).
|
|
113
|
+
let targetLib = libPath;
|
|
114
|
+
if (command === 'verify-env') {
|
|
115
|
+
targetLib = path.join(__dirname, 'lib', 'commands', 'mission-start.js');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (existsSyncFn(targetLib)) {
|
|
119
|
+
if (!READ_ONLY_COMMANDS.has(command)) {
|
|
120
|
+
const initResult = ensureStandaloneGitRepoFn(cwdFn());
|
|
121
|
+
if (initResult && initResult.failed) {
|
|
122
|
+
errorFn(fmt.status('FAIL', `Git init: ${initResult.message}`));
|
|
123
|
+
exitFn(1);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (initResult && initResult.initialized) {
|
|
127
|
+
logFn(fmt.status('INFO', `Initialized git repository for standalone parallix in ${cwdFn()} (branch ${initResult.branch || 'main'}).`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cmdModule = requireFn(targetLib);
|
|
132
|
+
if (typeof cmdModule === 'function') {
|
|
133
|
+
await cmdModule(args.slice(1), { command });
|
|
134
|
+
} else {
|
|
135
|
+
errorFn(fmt.status('FAIL', `Command module '${command}' does not export a function.`));
|
|
136
|
+
exitFn(1);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
const aliases = loadAliasesFn({ rootDir: cwdFn() });
|
|
140
|
+
const canonical = resolveAlias(command, aliases);
|
|
141
|
+
if (canonical) {
|
|
142
|
+
logFn(fmt.status('INFO', `Resolving alias ${command} → ${canonical}`));
|
|
143
|
+
await main([canonical, ...args.slice(1)], { existsSyncFn, cwdFn, ensureStandaloneGitRepoFn, requireFn, printUsageFn, exitFn, errorFn, logFn, loadAliasesFn });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
errorFn(fmt.status('FAIL', `Unknown command: ${command}`));
|
|
148
|
+
const suggestion = suggestCommand(command);
|
|
149
|
+
if (suggestion) {
|
|
150
|
+
errorFn(fmt.status('INFO', `Did you mean: px ${suggestion}${buildSuggestionSuffix(suggestion)}`));
|
|
151
|
+
}
|
|
152
|
+
printUsageFn();
|
|
153
|
+
exitFn(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildSuggestionSuffix(command) {
|
|
158
|
+
if (command === 'diff' || command === 'resolve-conflict') {
|
|
159
|
+
return ' <slug>';
|
|
160
|
+
}
|
|
161
|
+
if (command === 'checkpoint') {
|
|
162
|
+
return ' <slug> <cp-name> "<next-action>"';
|
|
163
|
+
}
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function suggestCommand(input) {
|
|
168
|
+
if (!input) return null;
|
|
169
|
+
const normalizedInput = input.toLowerCase();
|
|
170
|
+
let best = null;
|
|
171
|
+
|
|
172
|
+
for (const candidate of KNOWN_COMMANDS) {
|
|
173
|
+
const distance = levenshteinDistance(normalizedInput, candidate);
|
|
174
|
+
if (distance > 2) continue;
|
|
175
|
+
if (!best || distance < best.distance) {
|
|
176
|
+
best = { candidate, distance };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return best ? best.candidate : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function levenshteinDistance(a, b) {
|
|
184
|
+
const rows = a.length + 1;
|
|
185
|
+
const cols = b.length + 1;
|
|
186
|
+
const dp = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < rows; i += 1) dp[i][0] = i;
|
|
189
|
+
for (let j = 0; j < cols; j += 1) dp[0][j] = j;
|
|
190
|
+
|
|
191
|
+
for (let i = 1; i < rows; i += 1) {
|
|
192
|
+
for (let j = 1; j < cols; j += 1) {
|
|
193
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
194
|
+
dp[i][j] = Math.min(
|
|
195
|
+
dp[i - 1][j] + 1,
|
|
196
|
+
dp[i][j - 1] + 1,
|
|
197
|
+
dp[i - 1][j - 1] + cost
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return dp[a.length][b.length];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printUsage() {
|
|
206
|
+
fmt.log.plain(`
|
|
207
|
+
Usage: px <command> [args]
|
|
208
|
+
|
|
209
|
+
${fmt.bold('Core Commands:')}
|
|
210
|
+
mission-start [<slug>] Implementer's startup preflight.
|
|
211
|
+
verify-env Diagnostic preflight: prints a USABLE / NOT USABLE verdict with remediation.
|
|
212
|
+
verify [<area>] Run the configured repository verification gate.
|
|
213
|
+
setup Interactive setup wizard: writes config, bootstraps Forgejo, and verifies the install.
|
|
214
|
+
setup-review Legacy Forgejo-only bootstrap for tokens, repo creation, and git review remote.
|
|
215
|
+
draft [<slug>] Mission setup automation (branch, worktree, MISSION.md).
|
|
216
|
+
active [<slug>] Run preflight then launch the execute agent in the mission worktree.
|
|
217
|
+
status [<slug>] Unified mission and repository overview.
|
|
218
|
+
checkpoint [<slug>] <cp> "<next>" Verify, commit, and push checkpoint.
|
|
219
|
+
review [<slug>] [--verify|--submit|--push [--force]|--comment "<msg>"|--comment-file <path>|--submit-review <outcome> [--message "<msg>"|--message-file <path>]|--start|--continue] [--implementer <a>] [--reviewer <a>] [--focus <f>] [--max-attempts <n>] [--dry-run] [--reset] [--no-gate]
|
|
220
|
+
handoff [<slug>] [--no-gate] [--force] Sync, push, and transition mission to review.
|
|
221
|
+
integrate [<slug>] [--dry-run] [--no-integration-gates] Land a reviewed mission into the local integration checkout on main. --no-integration-gates skips integration-time staging/e2e gates.
|
|
222
|
+
resolve-conflict [<slug>] Detect merge conflicts in the mission worktree and emit resolution guidance.
|
|
223
|
+
rebase [<slug>] [--push] Rebase mission branch onto the primary integration branch (main) with auto-resolution of mission-specific conflicts.
|
|
224
|
+
diff [<slug>] Launch the primary local diff tool for branch-vs-main review.
|
|
225
|
+
stats [<csv_file>|--csv-file <path>] [--today YYYY-MM-DD|--from YYYY-MM-DD --to YYYY-MM-DD] [--output <file>] Print parallix weekly or range tables from <PARALLIX_HOME>/stats.csv; legacy retrospective CSVs remain supported.
|
|
226
|
+
|
|
227
|
+
${fmt.bold('Notes:')}
|
|
228
|
+
- <slug> is optional if it can be inferred from the current branch, directory name, or git worktree.
|
|
229
|
+
- When provided, <slug> MUST be the lowercase Backlog task key (e.g., task-073).
|
|
230
|
+
- Mistyped parallix subcommands print the closest supported \`px ...\` suggestion when the match is unambiguous.
|
|
231
|
+
- Run \`px stats --help\` for pre-integration stats preview examples.
|
|
232
|
+
- No npm dependencies — requires Node.js built-ins only.
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (require.main === module) {
|
|
237
|
+
main();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
KNOWN_COMMANDS,
|
|
242
|
+
main,
|
|
243
|
+
printUsage,
|
|
244
|
+
printAliases,
|
|
245
|
+
suggestCommand,
|
|
246
|
+
buildSuggestionSuffix,
|
|
247
|
+
levenshteinDistance,
|
|
248
|
+
deriveAliases,
|
|
249
|
+
resolveAlias,
|
|
250
|
+
};
|
package/lib/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# parallix/lib/ — Grouped Module Layout
|
|
2
|
+
|
|
3
|
+
Five subdirectories, each with a single responsibility. No flat `.js` files at the lib root (only `index.js` for backward compatibility).
|
|
4
|
+
|
|
5
|
+
| Directory | Responsibility |
|
|
6
|
+
|-----------|----------------|
|
|
7
|
+
| `agents/` | AI agent launchers, session management, and rate-limit detection |
|
|
8
|
+
| `commands/` | Workflow command handlers — each file is a `node parallix <cmd>` entry point |
|
|
9
|
+
| `core/` | Shared infrastructure with no command-handler imports (pure utilities) |
|
|
10
|
+
| `review/` | Review subsystem: artifacts, commands, loop, polling, prompts, state |
|
|
11
|
+
| `tools/` | External integrations and supporting workflow libraries |
|
|
12
|
+
|
|
13
|
+
See `LAYOUT_DECISION.md` in the mission directory for the full dependency graph analysis and rationale.
|