pan-wizard 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -8
- package/agents/pan-conductor.md +14 -1
- package/agents/pan-release.md +58 -0
- package/assets/pan-avatar.png +0 -0
- package/assets/pan-developer.png +0 -0
- package/assets/pan-docs-header.png +0 -0
- package/assets/pan-hero.png +0 -0
- package/assets/pan-logo-2000-transparent.svg +11 -30
- package/assets/pan-logo-2000.svg +12 -43
- package/assets/pan-logo-lockup.svg +11 -0
- package/assets/pan-mark.svg +7 -0
- package/assets/pan-orchestration.png +0 -0
- package/assets/pan-readme-hero.png +0 -0
- package/assets/terminal.svg +39 -119
- package/commands/pan/army.md +169 -0
- package/commands/pan/dashboard.md +25 -0
- package/commands/pan/focus-auto.md +32 -4
- package/commands/pan/hud.md +91 -0
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/campaign.cjs +198 -0
- package/pan-wizard-core/bin/lib/constants.cjs +8 -0
- package/pan-wizard-core/bin/lib/core.cjs +11 -0
- package/pan-wizard-core/bin/lib/focus.cjs +13 -1
- package/pan-wizard-core/bin/lib/hud.cjs +887 -0
- package/pan-wizard-core/bin/lib/squads.cjs +152 -0
- package/pan-wizard-core/bin/lib/worktree.cjs +123 -0
- package/pan-wizard-core/bin/pan-tools.cjs +68 -0
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +56 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squads — agent groupings for the bot-army model (ADR-0032).
|
|
3
|
+
*
|
|
4
|
+
* A squad is a named, tool-scoped, model-tiered grouping of existing PAN
|
|
5
|
+
* agents under the pan-conductor coordinator. This module is a registry +
|
|
6
|
+
* resolver only — it modifies no agent and changes no execution path. The
|
|
7
|
+
* army campaign command (ADR-0033) consumes it; until then it is inert.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { output, error } = require('./core.cjs');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Coordinator + worker/utility agents that are NOT squad members.
|
|
16
|
+
* - coordinator: the top of the hierarchy (Tier 0).
|
|
17
|
+
* - workers: cheap narrow-job agents (Tier 2) + standalone utility agents
|
|
18
|
+
* invoked directly by commands, not delegated through a squad.
|
|
19
|
+
*/
|
|
20
|
+
const COORDINATOR = 'pan-conductor';
|
|
21
|
+
const WORKERS = Object.freeze([
|
|
22
|
+
'pan-document_code', // Haiku-tier codebase mapper
|
|
23
|
+
'pan-distiller', // Haiku-tier code-bloat optimizer
|
|
24
|
+
'pan-optimizer', // optimization loop
|
|
25
|
+
'pan-experiment-runner', // self-improvement loop
|
|
26
|
+
'pan-knowledge', // retrieval/Q&A
|
|
27
|
+
'pan-counterfactual', // what-if worktree replay
|
|
28
|
+
'pan-previewer', // foresight synthesis
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The four squads, keyed by lifecycle role. `tier` is a PAN model tier
|
|
33
|
+
* (resolve-model maps it to a provider model); `access` is the least-privilege
|
|
34
|
+
* tool contract the conductor grants when delegating to the squad.
|
|
35
|
+
*/
|
|
36
|
+
const SQUADS = Object.freeze({
|
|
37
|
+
architecture: Object.freeze({
|
|
38
|
+
label: 'Architecture',
|
|
39
|
+
tier: 'reasoning',
|
|
40
|
+
access: 'read-only',
|
|
41
|
+
summary: 'Designs the system before code — contract-first.',
|
|
42
|
+
agents: Object.freeze([
|
|
43
|
+
'pan-roadmapper', 'pan-planner', 'pan-plan-checker',
|
|
44
|
+
'pan-project-researcher', 'pan-phase-researcher', 'pan-research-synthesizer',
|
|
45
|
+
]),
|
|
46
|
+
}),
|
|
47
|
+
build: Object.freeze({
|
|
48
|
+
label: 'Build',
|
|
49
|
+
tier: 'reasoning',
|
|
50
|
+
access: 'read-write-bash',
|
|
51
|
+
summary: 'Turns design and contracts into committed code.',
|
|
52
|
+
agents: Object.freeze(['pan-executor']),
|
|
53
|
+
}),
|
|
54
|
+
quality: Object.freeze({
|
|
55
|
+
label: 'Quality',
|
|
56
|
+
tier: 'mid',
|
|
57
|
+
access: 'read-only',
|
|
58
|
+
summary: 'Adversarially breaks what Build makes before users do.',
|
|
59
|
+
agents: Object.freeze([
|
|
60
|
+
'pan-reviewer', 'pan-hardener', 'pan-meta-reviewer',
|
|
61
|
+
'pan-verifier', 'pan-integration-checker', 'pan-debugger',
|
|
62
|
+
]),
|
|
63
|
+
}),
|
|
64
|
+
release: Object.freeze({
|
|
65
|
+
label: 'Release',
|
|
66
|
+
tier: 'mid',
|
|
67
|
+
access: 'always-ask',
|
|
68
|
+
summary: 'Ships safely behind a human gate; rolls back fast.',
|
|
69
|
+
agents: Object.freeze(['pan-release']),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const SQUAD_NAMES = Object.freeze(Object.keys(SQUADS));
|
|
74
|
+
|
|
75
|
+
/** @returns {Array<{name, label, tier, access, summary, agent_count}>} */
|
|
76
|
+
function listSquads() {
|
|
77
|
+
return SQUAD_NAMES.map(name => {
|
|
78
|
+
const s = SQUADS[name];
|
|
79
|
+
return { name, label: s.label, tier: s.tier, access: s.access, summary: s.summary, agent_count: s.agents.length };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @returns {object|null} the squad record (with name) or null if unknown. */
|
|
84
|
+
function getSquad(name) {
|
|
85
|
+
const s = SQUADS[name];
|
|
86
|
+
return s ? { name, ...s, agents: [...s.agents] } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reverse lookup: which squad owns an agent? @returns {string|null} */
|
|
90
|
+
function squadForAgent(agent) {
|
|
91
|
+
for (const name of SQUAD_NAMES) {
|
|
92
|
+
if (SQUADS[name].agents.includes(agent)) return name;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate the roster against the set of real agents.
|
|
99
|
+
* @param {string[]} knownAgents - agent names that exist on disk
|
|
100
|
+
* @returns {{ ok: boolean, missing: string[], unassigned: string[] }}
|
|
101
|
+
* missing = squad members with no agent file
|
|
102
|
+
* unassigned = real agents that are neither coordinator, worker, nor squad member
|
|
103
|
+
*/
|
|
104
|
+
function validateRoster(knownAgents) {
|
|
105
|
+
const known = new Set(knownAgents);
|
|
106
|
+
const missing = [];
|
|
107
|
+
for (const name of SQUAD_NAMES) {
|
|
108
|
+
for (const a of SQUADS[name].agents) {
|
|
109
|
+
if (!known.has(a)) missing.push(a);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const assigned = new Set([COORDINATOR, ...WORKERS]);
|
|
113
|
+
for (const name of SQUAD_NAMES) for (const a of SQUADS[name].agents) assigned.add(a);
|
|
114
|
+
const unassigned = knownAgents.filter(a => !assigned.has(a));
|
|
115
|
+
return { ok: missing.length === 0 && unassigned.length === 0, missing, unassigned };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function cmdSquadList(raw) {
|
|
121
|
+
const squads = listSquads();
|
|
122
|
+
const human = squads
|
|
123
|
+
.map(s => `${s.label.padEnd(13)} ${s.tier.padEnd(10)} ${s.access.padEnd(16)} ${s.agent_count} agents`)
|
|
124
|
+
.join('\n');
|
|
125
|
+
output({ squads, coordinator: COORDINATOR, workers: [...WORKERS] }, raw, human);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cmdSquadShow(name, raw) {
|
|
129
|
+
const s = getSquad(name);
|
|
130
|
+
if (!s) {
|
|
131
|
+
return error(`Unknown squad "${name}". Available: ${SQUAD_NAMES.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
const human = [
|
|
134
|
+
`${s.label} squad — ${s.tier} tier · ${s.access}`,
|
|
135
|
+
s.summary,
|
|
136
|
+
s.agents.length ? 'Agents: ' + s.agents.join(', ') : 'Agents: (none — git-tool driven)',
|
|
137
|
+
].join('\n');
|
|
138
|
+
output(s, raw, human);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
SQUADS,
|
|
143
|
+
SQUAD_NAMES,
|
|
144
|
+
COORDINATOR,
|
|
145
|
+
WORKERS,
|
|
146
|
+
listSquads,
|
|
147
|
+
getSquad,
|
|
148
|
+
squadForAgent,
|
|
149
|
+
validateRoster,
|
|
150
|
+
cmdSquadList,
|
|
151
|
+
cmdSquadShow,
|
|
152
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree — branch-per-agent isolation for the bot army (ADR-0033).
|
|
3
|
+
*
|
|
4
|
+
* The Build squad parallelizes by giving each builder its own git worktree
|
|
5
|
+
* on its own `army/<task>` branch, so concurrent agents never touch the same
|
|
6
|
+
* working tree or the same file. Generalizes the worktree primitive proven in
|
|
7
|
+
* whatif.cjs; the army campaign command drives it. Zero deps, synchronous,
|
|
8
|
+
* cross-platform (delegates to execGit).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execGit, isGitRepo, toPosix, generateSlugInternal, output, error } = require('./core.cjs');
|
|
15
|
+
|
|
16
|
+
const ARMY_BRANCH_PREFIX = 'army/';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create an isolated worktree + branch for one army task.
|
|
20
|
+
* @param {string} cwd - main project root
|
|
21
|
+
* @param {string} task - free-text task name (slugified for branch/path)
|
|
22
|
+
* @param {Object} [opts] - { base: ref (default 'HEAD'), worktree_root }
|
|
23
|
+
* @returns {{worktree_path, branch, base}|{error}}
|
|
24
|
+
*/
|
|
25
|
+
function createTaskWorktree(cwd, task, opts) {
|
|
26
|
+
if (!task || !String(task).trim()) return { error: 'task name required' };
|
|
27
|
+
if (!isGitRepo(cwd)) return { error: 'Not a git repo — branch-per-agent requires git worktree support' };
|
|
28
|
+
|
|
29
|
+
const slug = generateSlugInternal(String(task)).slice(0, 40);
|
|
30
|
+
const branch = `${ARMY_BRANCH_PREFIX}${slug}`;
|
|
31
|
+
const worktreeRoot = opts?.worktree_root
|
|
32
|
+
|| path.join(path.dirname(path.resolve(cwd)), `pan-army-${slug}`);
|
|
33
|
+
const base = opts?.base || 'HEAD';
|
|
34
|
+
|
|
35
|
+
const result = execGit(cwd, ['worktree', 'add', '-b', branch, worktreeRoot, base]);
|
|
36
|
+
if (result.exitCode !== 0) {
|
|
37
|
+
return { error: `git worktree add failed: ${result.stderr}` };
|
|
38
|
+
}
|
|
39
|
+
return { worktree_path: toPosix(worktreeRoot), branch, base };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Remove an army worktree + its branch. Best-effort; warnings surfaced.
|
|
44
|
+
* @returns {{removed: true, warnings: string[]}|{error}}
|
|
45
|
+
*/
|
|
46
|
+
function removeTaskWorktree(cwd, worktreePath, branch, opts) {
|
|
47
|
+
if (!isGitRepo(cwd)) return { error: 'Not a git repo' };
|
|
48
|
+
const warnings = [];
|
|
49
|
+
const rmArgs = ['worktree', 'remove'];
|
|
50
|
+
if (opts?.force === true) rmArgs.push('--force');
|
|
51
|
+
rmArgs.push(worktreePath);
|
|
52
|
+
const rm = execGit(cwd, rmArgs);
|
|
53
|
+
if (rm.exitCode !== 0) warnings.push(`worktree remove: ${rm.stderr.trim()}`);
|
|
54
|
+
|
|
55
|
+
if (branch) {
|
|
56
|
+
// Only delete branches we created (army/ prefix), and only if not checked out.
|
|
57
|
+
if (branch.startsWith(ARMY_BRANCH_PREFIX)) {
|
|
58
|
+
const del = execGit(cwd, ['branch', '-D', branch]);
|
|
59
|
+
if (del.exitCode !== 0) warnings.push(`branch -D ${branch}: ${del.stderr.trim()}`);
|
|
60
|
+
} else {
|
|
61
|
+
warnings.push(`refused to delete non-army branch ${branch}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { removed: true, warnings };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List the army worktrees currently registered (army/ branches only).
|
|
69
|
+
* Parses `git worktree list --porcelain`.
|
|
70
|
+
* @returns {Array<{worktree, branch}>}
|
|
71
|
+
*/
|
|
72
|
+
function listArmyWorktrees(cwd) {
|
|
73
|
+
if (!isGitRepo(cwd)) return [];
|
|
74
|
+
const r = execGit(cwd, ['worktree', 'list', '--porcelain']);
|
|
75
|
+
if (r.exitCode !== 0) return [];
|
|
76
|
+
const out = [];
|
|
77
|
+
let current = {};
|
|
78
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
79
|
+
if (line.startsWith('worktree ')) {
|
|
80
|
+
current = { worktree: toPosix(line.slice('worktree '.length).trim()) };
|
|
81
|
+
} else if (line.startsWith('branch ')) {
|
|
82
|
+
const ref = line.slice('branch '.length).trim().replace('refs/heads/', '');
|
|
83
|
+
current.branch = ref;
|
|
84
|
+
if (ref.startsWith(ARMY_BRANCH_PREFIX)) out.push({ ...current });
|
|
85
|
+
} else if (line === '') {
|
|
86
|
+
current = {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function cmdWorktreeList(cwd, raw) {
|
|
95
|
+
const trees = listArmyWorktrees(cwd);
|
|
96
|
+
const human = trees.length
|
|
97
|
+
? trees.map(t => `${t.branch} → ${t.worktree}`).join('\n')
|
|
98
|
+
: 'No army worktrees';
|
|
99
|
+
output({ worktrees: trees, count: trees.length }, raw, human);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cmdWorktreeCreate(cwd, task, raw, opts) {
|
|
103
|
+
const r = createTaskWorktree(cwd, task, opts);
|
|
104
|
+
if (r.error) return error(r.error);
|
|
105
|
+
output(r, raw, `${r.branch} → ${r.worktree_path}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cmdWorktreeRemove(cwd, worktreePath, branch, raw, opts) {
|
|
109
|
+
if (!worktreePath) return error('worktree path required');
|
|
110
|
+
const r = removeTaskWorktree(cwd, worktreePath, branch, opts);
|
|
111
|
+
if (r.error) return error(r.error);
|
|
112
|
+
output(r, raw, r.warnings.length ? r.warnings.join('\n') : 'removed');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
ARMY_BRANCH_PREFIX,
|
|
117
|
+
createTaskWorktree,
|
|
118
|
+
removeTaskWorktree,
|
|
119
|
+
listArmyWorktrees,
|
|
120
|
+
cmdWorktreeList,
|
|
121
|
+
cmdWorktreeCreate,
|
|
122
|
+
cmdWorktreeRemove,
|
|
123
|
+
};
|
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
* Pre-Flight & Dashboard:
|
|
148
148
|
* preflight [phase|batch] Validate execution prerequisites
|
|
149
149
|
* dashboard Aggregated project status overview
|
|
150
|
+
* hud [--out f] [--open] [--stdout] Single-page HTML army + project dashboard
|
|
150
151
|
*
|
|
151
152
|
* Session Learnings:
|
|
152
153
|
* learnings extract Extract patterns from session data
|
|
@@ -856,6 +857,16 @@ async function main() {
|
|
|
856
857
|
break;
|
|
857
858
|
}
|
|
858
859
|
|
|
860
|
+
case 'hud': {
|
|
861
|
+
const hud = require('./lib/hud.cjs');
|
|
862
|
+
hud.cmdHud(cwd, {
|
|
863
|
+
out: getArgValue(args, '--out'),
|
|
864
|
+
open: args.includes('--open'),
|
|
865
|
+
stdout: args.includes('--stdout'),
|
|
866
|
+
}, raw);
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
869
|
+
|
|
859
870
|
case 'learnings': {
|
|
860
871
|
const subcommand = args[1];
|
|
861
872
|
if (subcommand === 'extract') {
|
|
@@ -1049,6 +1060,63 @@ async function main() {
|
|
|
1049
1060
|
break;
|
|
1050
1061
|
}
|
|
1051
1062
|
|
|
1063
|
+
case 'squad': {
|
|
1064
|
+
const squads = require('./lib/squads.cjs');
|
|
1065
|
+
const subcommand = args[1];
|
|
1066
|
+
if (subcommand === 'list' || !subcommand) {
|
|
1067
|
+
squads.cmdSquadList(raw);
|
|
1068
|
+
} else if (subcommand === 'show') {
|
|
1069
|
+
squads.cmdSquadShow(args[2], raw);
|
|
1070
|
+
} else {
|
|
1071
|
+
error('Unknown squad subcommand. Available: list, show');
|
|
1072
|
+
}
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
case 'worktree': {
|
|
1077
|
+
const worktree = require('./lib/worktree.cjs');
|
|
1078
|
+
const subcommand = args[1];
|
|
1079
|
+
if (subcommand === 'list' || !subcommand) {
|
|
1080
|
+
worktree.cmdWorktreeList(cwd, raw);
|
|
1081
|
+
} else if (subcommand === 'create') {
|
|
1082
|
+
worktree.cmdWorktreeCreate(cwd, args[2], raw, { base: getArgValue(args, '--base') });
|
|
1083
|
+
} else if (subcommand === 'remove') {
|
|
1084
|
+
worktree.cmdWorktreeRemove(cwd, args[2], getArgValue(args, '--branch'), raw, { force: args.includes('--force') });
|
|
1085
|
+
} else {
|
|
1086
|
+
error('Unknown worktree subcommand. Available: list, create, remove');
|
|
1087
|
+
}
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case 'campaign': {
|
|
1092
|
+
const campaign = require('./lib/campaign.cjs');
|
|
1093
|
+
const subcommand = args[1];
|
|
1094
|
+
if (subcommand === 'schedule') {
|
|
1095
|
+
const budget = getArgValue(args, '--daily-budget');
|
|
1096
|
+
campaign.cmdCampaignSchedule(cwd, {
|
|
1097
|
+
goal: getArgValue(args, '--goal'),
|
|
1098
|
+
source: getArgValue(args, '--source'),
|
|
1099
|
+
cadence: getArgValue(args, '--cadence', 'daily'),
|
|
1100
|
+
daily_budget: budget != null ? Number(budget) : undefined,
|
|
1101
|
+
enabled: args.includes('--disable') ? false : undefined,
|
|
1102
|
+
paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
|
|
1103
|
+
}, raw);
|
|
1104
|
+
} else if (subcommand === 'status' || !subcommand) {
|
|
1105
|
+
campaign.cmdCampaignStatus(cwd, raw);
|
|
1106
|
+
} else if (subcommand === 'due') {
|
|
1107
|
+
campaign.cmdCampaignDue(cwd, raw);
|
|
1108
|
+
} else if (subcommand === 'record-run') {
|
|
1109
|
+
const r = campaign.recordRun(cwd, {
|
|
1110
|
+
items_landed: Number(getArgValue(args, '--items', 0)),
|
|
1111
|
+
points_used: Number(getArgValue(args, '--points', 0)),
|
|
1112
|
+
});
|
|
1113
|
+
if (r.error) { error(r.error); } else { output(r, raw, `recorded · next ${r.next_due}`); }
|
|
1114
|
+
} else {
|
|
1115
|
+
error('Unknown campaign subcommand. Available: schedule, status, due, record-run');
|
|
1116
|
+
}
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1052
1120
|
case 'bus': {
|
|
1053
1121
|
const subcommand = args[1];
|
|
1054
1122
|
if (subcommand === 'publish') {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: autonomous-loop
|
|
3
|
+
last_updated: 2026-06-12T00:00:00.000Z
|
|
4
|
+
patterns:
|
|
5
|
+
- id: P-310
|
|
6
|
+
summary: Autonomous build loops should fan out research and verify in parallel but keep implement/build a single serial step, then seal with one clean build at loop end
|
|
7
|
+
promoted_at: 2026-06-12T00:00:00.000Z
|
|
8
|
+
source_experiments: [montyhall-focus-loop]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Autonomous Loop (AI-derived)
|
|
12
|
+
|
|
13
|
+
> Hand-promoted from a downstream `/focus-loop` campaign command (MontyHall compiler project) and generalized in ADR-0031. Patterns are **advisory** — orchestrators weight them against current context.
|
|
14
|
+
|
|
15
|
+
## P-310 — Parallel-research → single-serial-build → parallel-verify, then clean-seal
|
|
16
|
+
|
|
17
|
+
**Evidence:** A backlog-driven autonomous loop that landed many items per run converged on this shape (the "cc#67/cc#68" discipline): research and verification are read-only and parallelize cheaply via the Workflow tool, but the implement/build step mutates shared state and must be a *single* serial actor. Per-item incremental commits twice produced cross-item orphans — a symbol defined only in an uncommitted file, or a combined state that didn't build clean — which only a from-scratch build at loop end caught.
|
|
18
|
+
|
|
19
|
+
**Rule:** For any pick → build → verify → commit → repeat loop:
|
|
20
|
+
|
|
21
|
+
1. **Fan out research in parallel** (read-only agents): map the substrate, scope the honest-partial boundary, probe for support. Mutate nothing.
|
|
22
|
+
2. **Implement/build is exactly ONE serial actor** — never inside a `parallel()`. If the project's build trees corrupt under concurrency, enforce at-most-one-builder across the whole loop (a per-project opt-in, not a universal law).
|
|
23
|
+
3. **Fan out verify in parallel** (read-only) over the already-built tree: correctness / security / honesty lenses.
|
|
24
|
+
4. **Commit-quality gates:** a *staging-miss guard* (no implementer-touched file left unstaged — never `git add -A`, never a hand-picked subset) and an *orphan audit* (HEAD references no symbol defined only in an uncommitted file).
|
|
25
|
+
5. **Clean-build seal once at loop end:** per-item builds are incremental for speed; a single from-scratch build + full verification after the last item catches cross-item orphans the incremental builds hid.
|
|
26
|
+
6. **Rank the backlog from the CURRENT document** (value/effort), never a frozen ID list, so the order never goes stale; honest-partial aggressively and strike only what landed.
|
|
27
|
+
|
|
28
|
+
**Applies in:** `/pan:focus-auto` (`--source backlog`, `--parallel-research`/`--parallel-verify`/`--clean-seal`; ADR-0031), any Workflow-orchestrated build campaign, hierarchical exec (`pan-conductor`).
|
|
29
|
+
|
|
30
|
+
## P-330 — Scale agents into a coordinated army with squads, worktree isolation, and a human-gated ship
|
|
31
|
+
|
|
32
|
+
**Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a delegation-only coordinator (never codes) fans work to role-scoped *squads* (architecture/build/quality/release), the build squad parallelizes by giving each agent its own branch + git worktree (so concurrent builders never touch the same file), quality is adversarial and read-only, and the path to a protected branch is a human-approved gate, not a bot merge.
|
|
33
|
+
|
|
34
|
+
**Rule:** When scaling beyond a single agent:
|
|
35
|
+
|
|
36
|
+
1. **Coordinator delegates, never codes** — its tools are delegation-only; it plans, decomposes, and routes to squads, then aggregates tight summaries.
|
|
37
|
+
2. **Group agents into role-scoped squads with least-privilege tools** — design read-only, build read/write, quality read-only/adversarial, release always-ask. Resolve the roster from data, not hardcoded prompt lists.
|
|
38
|
+
3. **Parallelize by isolation, not by hope** — one branch + worktree per concurrent builder; never two agents in one tree. Serialize builds only where the build tree corrupts under concurrency (a per-project opt-in).
|
|
39
|
+
4. **The mutating boundary is human-gated** — merging to a protected branch is `always-ask`; recovery is revert / previous tag, never force-push or history rewrite.
|
|
40
|
+
5. **The harness scales with the army, not after it** — depth caps, spawn/budget ceilings, and an abort kill-switch checked before every spawn are mandatory; a longer loop must not relax a single cap. Power and safety are the same investment.
|
|
41
|
+
|
|
42
|
+
**Applies in:** `/pan:army` (ADR-0033), `pan-conductor` campaign mode, `squads.cjs` / `worktree.cjs`, any multi-agent delivery.
|
|
43
|
+
|
|
44
|
+
## P-340 — Schedule autonomy as a due-check the host fires, not a daemon you embed
|
|
45
|
+
|
|
46
|
+
**Evidence:** Making the army run over days (ADR-0034) surfaced the right boundary for a prompt-driven tool that lives inside a host session: it cannot wake itself while the session is closed, and pretending to (an embedded scheduler/daemon spawning agents in the background) is both a false promise and a security surface. The durable design split ownership — PAN owns the schedule *descriptor* and the *decision whether a run is due*; the host scheduler owns *firing* it.
|
|
47
|
+
|
|
48
|
+
**Rule:** For "run X automatically over time" in a session-bound agent:
|
|
49
|
+
|
|
50
|
+
1. **Persist a schedule descriptor, not a timer** — cadence, next-due, per-day budget, enabled/paused — in project state.
|
|
51
|
+
2. **Expose a cheap, side-effect-free `due` check** the external trigger polls; make the *reason* explicit (`not_yet` / `paused` / `budget_exhausted_today` / `due`) so skips are auditable.
|
|
52
|
+
3. **Let the host fire it** — cron / routines / a session loop / a next-open nudge — rather than embedding a background daemon.
|
|
53
|
+
4. **Resume from persisted state**, advancing next-due and accruing per-day spend on each run; cap the day, not just the run.
|
|
54
|
+
5. **Never let a schedule lower an irreversible-action gate** — scheduled or not, the human approves the merge. Autonomy extends up to the irreversible step, never through it.
|
|
55
|
+
|
|
56
|
+
**Applies in:** `campaign.cjs` + `/pan:army --schedule` (ADR-0034), any cron/`/loop`-driven PAN automation, the self-improvement loop on a cadence.
|