axstack 0.20.30 → 0.20.31
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 +2 -2
- package/bin/axstack.js +1 -0
- package/docs/installation.md +12 -6
- package/docs/workflows.md +29 -10
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +46 -46
- package/profiles/presets/codex-only.json +50 -50
- package/profiles/presets/mixed.json +54 -54
- package/skills/axstack/references/autopilot.md +108 -0
- package/skills/axstack/references/contracts.md +5 -1
- package/skills/axstack/references/orca-runtime.md +25 -6
- package/skills/axstack/references/role-roster.md +7 -7
- package/skills/axstack/references/routing.md +16 -5
- package/skills/axstack/references/run-record.md +2 -0
- package/skills/axstack/scripts/resolve-models.js +46 -0
- package/skills/axstack-align/SKILL.md +8 -4
- package/skills/axstack-audit/SKILL.md +10 -2
- package/skills/axstack-implement/SKILL.md +23 -9
- package/skills/axstack-relay/SKILL.md +9 -2
- package/skills/axstack-review/SKILL.md +13 -7
- package/skills/axstack-spec/SKILL.md +5 -1
- package/skills/axstack-tickets/SKILL.md +6 -3
- package/skills/axstack-watch/SKILL.md +26 -13
- package/skills/axstack-watch/references/watch-runtime.md +11 -4
- package/src/installer.js +8 -0
- package/src/roles.js +38 -10
|
@@ -90,8 +90,10 @@ busy, missing, fenced, protected, or permission-held driver is never interrupted
|
|
|
90
90
|
or replaced.
|
|
91
91
|
|
|
92
92
|
The driver records one Notification policy: `axstack-relay` Telegram home only
|
|
93
|
-
for a user-decision hold
|
|
94
|
-
two
|
|
93
|
+
for a user-decision hold (including spec and npm approval), merge-ready or
|
|
94
|
+
merged milestones (at most two across implementation and release), or a
|
|
95
|
+
serious-risk hold.
|
|
96
|
+
Quiet ticks never notify.
|
|
95
97
|
|
|
96
98
|
The driver alone routes repair. Re-read remote head/base and native ownership.
|
|
97
99
|
Independent PRs may repair in parallel in separate Orca child worktrees within
|
|
@@ -116,8 +118,13 @@ re-read all feedback and approvals at the current head before readiness.
|
|
|
116
118
|
Re-reading approvals checks current state, not re-requesting review from a
|
|
117
119
|
human who already approved.
|
|
118
120
|
|
|
119
|
-
Stop the chosen wake only when every watched PR is merged or closed
|
|
120
|
-
|
|
121
|
+
Stop the chosen wake only when every watched PR is merged or closed and the
|
|
122
|
+
run's release step is settled or not applicable, the user cancels, or it
|
|
123
|
+
expires. Without an Autopilot or Release record, the release step is not
|
|
124
|
+
applicable to this watch. A required PR closed without merging records a
|
|
125
|
+
decision hold and the wake remains active while unexpired until the user
|
|
126
|
+
resolves scope, cancels, or the wake expires; the run is not release-eligible.
|
|
127
|
+
The driver stops a harness-native wake and verifies its stop receipt;
|
|
121
128
|
a failed or uncertain stop is a hold. Re-read membership and confirm no ambiguous
|
|
122
129
|
publication or unsettled pass; cancellation
|
|
123
130
|
prevents new work but does not prove running workers exited. The observer may
|
package/src/installer.js
CHANGED
|
@@ -635,8 +635,16 @@ export async function installBundle({
|
|
|
635
635
|
const newIds = new Set(bundle.bundleRoles.map((role) => role.id));
|
|
636
636
|
const addedRoleIds = bundle.bundleRoles.filter((role) => !oldIds.has(role.id)).map((role) => role.id);
|
|
637
637
|
const removedRoleIds = old.roles.filter((role) => role?.id && !newIds.has(role.id)).map((role) => role.id);
|
|
638
|
+
const oldById = new Map(old.roles.filter((role) => role?.id).map((role) => [role.id, role]));
|
|
639
|
+
const changedRoleModels = bundle.bundleRoles.flatMap((role) => {
|
|
640
|
+
const previous = oldById.get(role.id);
|
|
641
|
+
if (!previous) return [];
|
|
642
|
+
const fields = ['modelClass', 'model'].filter((field) => previous[field] !== role[field]);
|
|
643
|
+
return fields.length ? [`${role.id} (${fields.join(', ')})`] : [];
|
|
644
|
+
});
|
|
638
645
|
summary.addedRoleIds = addedRoleIds;
|
|
639
646
|
summary.removedRoleIds = removedRoleIds;
|
|
647
|
+
summary.changedRoleModels = changedRoleModels;
|
|
640
648
|
}
|
|
641
649
|
} catch { /* No reliable role-ID diff for malformed prior bytes. */ }
|
|
642
650
|
}
|
package/src/roles.js
CHANGED
|
@@ -4,19 +4,33 @@ const PROVIDER_BOUNDS = Object.freeze({
|
|
|
4
4
|
'claude-only': new Set(['claude']),
|
|
5
5
|
});
|
|
6
6
|
|
|
7
|
+
const CLASS_PROVIDERS = Object.freeze({
|
|
8
|
+
fable: 'claude', opus: 'claude', sonnet: 'claude', haiku: 'claude',
|
|
9
|
+
astra: 'codex', sol: 'codex', luna: 'codex',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function deriveModelClass(model) {
|
|
13
|
+
if (typeof model !== 'string') return null;
|
|
14
|
+
return model.match(/^gpt-(\d+(?:\.\d+)*)-(astra|sol|luna)$/)?.[2]
|
|
15
|
+
?? model.match(/^claude-(fable|opus|sonnet|haiku)-/)?.[1]
|
|
16
|
+
?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
const AUTHORED_ROUTES = Object.freeze({
|
|
8
20
|
mixed: {
|
|
9
|
-
'codex/
|
|
10
|
-
'claude/
|
|
21
|
+
'codex/sol': ['axstack-reviewer-secondary', 'claude/opus', 'medium'],
|
|
22
|
+
'claude/opus': ['axstack-reviewer-primary', 'codex/sol', 'high'],
|
|
11
23
|
},
|
|
12
24
|
'codex-only': {
|
|
13
|
-
'codex/
|
|
25
|
+
'codex/sol': ['axstack-reviewer-secondary', 'codex/luna', 'xhigh'],
|
|
14
26
|
},
|
|
15
27
|
'claude-only': {
|
|
16
|
-
'claude/
|
|
28
|
+
'claude/opus': ['axstack-reviewer-secondary', 'claude/sonnet', 'high'],
|
|
17
29
|
},
|
|
18
30
|
});
|
|
19
31
|
|
|
32
|
+
const roleClass = (role) => role.modelClass ?? deriveModelClass(role.model);
|
|
33
|
+
|
|
20
34
|
export function assertBundleRoles(roles) {
|
|
21
35
|
if (!Array.isArray(roles) || roles.length === 0) {
|
|
22
36
|
throw new Error('invalid bundle roles: expected a non-empty roles array');
|
|
@@ -36,12 +50,22 @@ export function assertBundleRoles(roles) {
|
|
|
36
50
|
}
|
|
37
51
|
if (ids.has(role.id)) throw new Error(`invalid bundle roles: duplicate role ID ${role.id}`);
|
|
38
52
|
ids.add(role.id);
|
|
39
|
-
|
|
53
|
+
const hasClass = Object.hasOwn(role, 'modelClass');
|
|
54
|
+
if (hasClass && (typeof role.modelClass !== 'string' || !Object.hasOwn(CLASS_PROVIDERS, role.modelClass))) {
|
|
55
|
+
throw new Error(`invalid bundle roles: ${role.id} has an unsupported modelClass`);
|
|
56
|
+
}
|
|
57
|
+
if (hasClass && role.provider !== CLASS_PROVIDERS[role.modelClass]) {
|
|
58
|
+
throw new Error(`invalid bundle roles: ${role.id} model class does not match provider`);
|
|
59
|
+
}
|
|
60
|
+
if (!hasClass && !Object.hasOwn(role, 'model')) {
|
|
40
61
|
throw new Error('invalid bundle roles: model must be a non-empty string or explicit null');
|
|
41
62
|
}
|
|
42
|
-
if (role.model !== null && (typeof role.model !== 'string' || role.model.trim() === '')) {
|
|
63
|
+
if (Object.hasOwn(role, 'model') && role.model !== null && (typeof role.model !== 'string' || role.model.trim() === '')) {
|
|
43
64
|
throw new Error('invalid bundle roles: model must be a non-empty string or explicit null');
|
|
44
65
|
}
|
|
66
|
+
if (hasClass && typeof role.model === 'string' && deriveModelClass(role.model) !== role.modelClass) {
|
|
67
|
+
throw new Error(`invalid bundle roles: ${role.id} model pin does not match model class`);
|
|
68
|
+
}
|
|
45
69
|
for (const field of ['icon', 'color', 'modeId', 'thinkingOptionId', 'notes']) {
|
|
46
70
|
if (role[field] !== undefined && typeof role[field] !== 'string') {
|
|
47
71
|
throw new Error(`invalid bundle roles: ${field} must be a string when present`);
|
|
@@ -75,7 +99,7 @@ export function assessRoleReadiness(roles, preset) {
|
|
|
75
99
|
if (!bounds.has(role.provider)) {
|
|
76
100
|
gaps.push(`${role.id} provider ${JSON.stringify(role.provider)} is outside ${preset} bounds (${[...bounds].join('|')})`);
|
|
77
101
|
}
|
|
78
|
-
if (!isIntentionalAbsence(role) && (typeof role.model !== 'string' || role.model.trim() === '')) {
|
|
102
|
+
if (!isIntentionalAbsence(role) && !Object.hasOwn(role, 'modelClass') && (typeof role.model !== 'string' || role.model.trim() === '')) {
|
|
79
103
|
gaps.push(`${role.id} requires a configured model`);
|
|
80
104
|
}
|
|
81
105
|
}
|
|
@@ -86,7 +110,11 @@ export function assessRoleReadiness(roles, preset) {
|
|
|
86
110
|
const primary = byId.get('axstack-reviewer-primary');
|
|
87
111
|
const secondary = byId.get('axstack-reviewer-secondary');
|
|
88
112
|
if (primary && secondary) {
|
|
89
|
-
|
|
113
|
+
const bothResolved = typeof primary.model === 'string' && typeof secondary.model === 'string';
|
|
114
|
+
const sameReviewer = bothResolved
|
|
115
|
+
? primary.model === secondary.model
|
|
116
|
+
: roleClass(primary) !== null && roleClass(primary) === roleClass(secondary);
|
|
117
|
+
if (sameReviewer) gaps.push('reviewer pair must use two distinct models');
|
|
90
118
|
if (preset === 'mixed' && primary.provider === secondary.provider) {
|
|
91
119
|
gaps.push('mixed reviewer pair must use different providers');
|
|
92
120
|
}
|
|
@@ -94,14 +122,14 @@ export function assessRoleReadiness(roles, preset) {
|
|
|
94
122
|
|
|
95
123
|
const author = byId.get('axstack-author');
|
|
96
124
|
if (author) {
|
|
97
|
-
const authorRoute = `${author.provider}/${author.model}`;
|
|
125
|
+
const authorRoute = `${author.provider}/${roleClass(author) ?? author.model}`;
|
|
98
126
|
const route = AUTHORED_ROUTES[preset]?.[authorRoute];
|
|
99
127
|
if (!route) {
|
|
100
128
|
gaps.push(`authored routing gap: unsupported axstack-author route ${authorRoute}`);
|
|
101
129
|
} else {
|
|
102
130
|
const [reviewerId, reviewerRoute, effort] = route;
|
|
103
131
|
const reviewer = byId.get(reviewerId);
|
|
104
|
-
if (!reviewer || `${reviewer.provider}/${reviewer
|
|
132
|
+
if (!reviewer || `${reviewer.provider}/${roleClass(reviewer)}` !== reviewerRoute || reviewer.thinkingOptionId !== effort) {
|
|
105
133
|
gaps.push(`authored routing gap: ${reviewerId} must be ${reviewerRoute}/${effort} for author ${authorRoute}`);
|
|
106
134
|
}
|
|
107
135
|
}
|