create-issflow 1.10.1 → 1.11.1
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/bin/cli.js +37 -12
- package/package.json +1 -1
- package/template/.issflow/README.md +4 -1
- package/template/.issflow/agents/planner.md +6 -1
- package/template/.issflow/agents/synthesizer.md +6 -4
- package/template/.issflow/commands/change-request.md +10 -2
- package/template/.issflow/commands/design.md +35 -20
- package/template/.issflow/commands/feature.md +6 -1
- package/template/.issflow/commands/overview.md +46 -7
- package/template/.issflow/commands/phase.md +50 -13
- package/template/.issflow/commands/propose.md +25 -7
- package/template/.issflow/commands/quick.md +2 -0
- package/template/.issflow/flow-config.json +11 -4
- package/template/.issflow/hooks/session-start.js +64 -128
- package/template/.issflow/istartsoft-flow/METHODOLOGY.md +57 -17
- package/template/.issflow/models/MODEL-MATRIX.md +33 -0
- package/template/.issflow/routing/model-router.js +66 -0
- package/template/.issflow/routing/policy.json +69 -0
- package/template/.issflow/scripts/entry-notice.js +20 -0
- package/template/.issflow/scripts/token-lint.js +58 -0
- package/template/.issflow/skills/ui-ux-pro-max/SKILL.md +7 -2
- package/template/.issflow/skills/ux-design/SKILL.md +3 -0
- package/template/.issflow/templates/ARCHITECTURE-template.md +7 -3
- package/template/.issflow/templates/PLAN-SCOPED.md +12 -0
- package/template/.issflow/templates/UX-PACK-template.md +19 -8
- package/template/.issflow/templates/design/DIRECTION.md +22 -0
- package/template/.issflow/templates/design/inventory.md +18 -0
- package/template/.issflow/templates/design/picker.html +165 -0
- package/template/.issflow/templates/design/platforms/line-oa.md +16 -0
- package/template/.issflow/templates/design/platforms/mobile-app.md +10 -0
- package/template/.issflow/templates/design/platforms/pwa.md +11 -0
- package/template/.issflow/templates/design/platforms/web-responsive.md +9 -0
- package/template/.issflow/templates/design/screens/SCREEN.md +19 -0
- package/template/.issflow/templates/design/tokens.css +47 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// SessionStart
|
|
3
|
-
// Pure Node, cross-platform (macOS / Windows / Linux) — no bash/jq/python needed.
|
|
2
|
+
// SessionStart — Tier A inject (~lean). stdout → agent context.
|
|
4
3
|
'use strict';
|
|
5
4
|
const fs = require('fs');
|
|
6
5
|
const path = require('path');
|
|
@@ -15,144 +14,81 @@ const read = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch (_) { ret
|
|
|
15
14
|
const exists = (f) => fs.existsSync(f);
|
|
16
15
|
const sh = (cmd) => { try { return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }); } catch (_) { return ''; } };
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
let cfg = {};
|
|
18
|
+
try { cfg = JSON.parse(read('.issflow/flow-config.json') || '{}'); } catch (_) {}
|
|
19
|
+
const maxStateLines = (cfg.context && cfg.context.injectMaxLines) || 25;
|
|
20
|
+
const staleHours = (cfg.delivery && cfg.delivery.staleAfterHours) || 24;
|
|
21
|
+
|
|
22
|
+
// Resume/compact: keep inject tiny (Pattern 1 — skip heavy bodies)
|
|
23
|
+
let source = 'startup';
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(0, 'utf8');
|
|
26
|
+
if (raw) {
|
|
27
|
+
const j = JSON.parse(raw);
|
|
28
|
+
source = j.source || j.hook_event_name || source;
|
|
29
|
+
}
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
const leanResume = /resume|compact/i.test(String(source));
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
emit('
|
|
23
|
-
emit('
|
|
24
|
-
|
|
25
|
-
emit('uncommitted: ' + uncommitted + ' file(s)');
|
|
26
|
-
for (const l of sh('git log --oneline -3').replace(/\n+$/, '').split('\n').filter(Boolean)) emit(' ' + l);
|
|
33
|
+
emit('=== iStartSoftFlow Tier-A (session) ===');
|
|
34
|
+
emit('source: ' + source);
|
|
35
|
+
emit('git: ' + (sh('git branch --show-current').trim() || 'n/a') +
|
|
36
|
+
' · dirty ' + sh('git status --short').split('\n').filter((l) => l.trim()).length);
|
|
27
37
|
emit('');
|
|
28
38
|
|
|
29
|
-
// 2. active state (cap size — STATE.md is meant to stay small)
|
|
30
39
|
const state = read('docs/STATE.md');
|
|
31
|
-
if (state
|
|
32
|
-
emit('
|
|
33
|
-
const sl = state.replace(/\n$/, '').split('\n');
|
|
34
|
-
for (const l of sl.slice(0, 40)) emit(l);
|
|
35
|
-
if (sl.length > 40) emit(`… (+${sl.length - 40} more — STATE.md should be small; trim it)`);
|
|
36
|
-
emit('');
|
|
40
|
+
if (!state) {
|
|
41
|
+
emit('STATE.md missing → /overview');
|
|
37
42
|
} else {
|
|
38
|
-
emit('## STATE
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
emit('## STATE (read first)');
|
|
44
|
+
const lines = state.replace(/\n$/, '').split('\n');
|
|
45
|
+
for (const l of lines.slice(0, maxStateLines)) emit(l);
|
|
46
|
+
if (lines.length > maxStateLines) emit(`… (+${lines.length - maxStateLines} more)`);
|
|
41
47
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const issues = read('docs/ISSUES.md');
|
|
60
|
-
if (issues !== null) {
|
|
61
|
-
const blocks = [];
|
|
62
|
-
let cur = null;
|
|
63
|
-
for (const l of issues.split('\n')) {
|
|
64
|
-
if (/^### /.test(l)) { if (cur) blocks.push(cur); cur = [l]; }
|
|
65
|
-
else if (cur) cur.push(l);
|
|
66
|
-
}
|
|
67
|
-
if (cur) blocks.push(cur);
|
|
68
|
-
const open = blocks.filter((b) => b.some((l) => /- \[ \]/.test(l)));
|
|
69
|
-
emit(`## ISSUES.md (${open.length} open) - grep this file before debugging anything`);
|
|
70
|
-
if (open.length) {
|
|
71
|
-
const flat = open.flat();
|
|
72
|
-
for (const l of flat.slice(0, 50)) emit(l);
|
|
73
|
-
if (flat.length > 50) emit('… (more — grep docs/ISSUES.md for full detail)');
|
|
74
|
-
} else {
|
|
75
|
-
emit('(no open issues)');
|
|
48
|
+
// Delivery / WAITING_STALE nudge
|
|
49
|
+
const waitSince = (state.match(/waiting_since:\s*(\S+)/) || [])[1];
|
|
50
|
+
const status = (state.match(/^\s*status:\s*(\S+)/m) || [])[1] || '';
|
|
51
|
+
const waitingFor = (state.match(/waiting_for:\s*(.+)/) || [])[1];
|
|
52
|
+
const nextHuman = (state.match(/next_human_action:\s*(.+)/) || [])[1];
|
|
53
|
+
const agentEta = (state.match(/agent_eta:\s*(\S+)/) || [])[1];
|
|
54
|
+
if (agentEta && agentEta !== 'none') emit('delivery.agent_eta: ' + agentEta + ' · status: ' + status);
|
|
55
|
+
if (/WAITING/i.test(status) || (waitingFor && waitingFor.trim() !== 'none')) {
|
|
56
|
+
emit('WAITING_ON_HUMAN: ' + (waitingFor || status).trim());
|
|
57
|
+
if (nextHuman) emit('next_human_action: ' + nextHuman.trim());
|
|
58
|
+
if (waitSince && waitSince !== 'none') {
|
|
59
|
+
const since = Date.parse(waitSince);
|
|
60
|
+
if (!Number.isNaN(since)) {
|
|
61
|
+
const hours = (Date.now() - since) / 3600000;
|
|
62
|
+
if (hours >= staleHours) emit('WAITING_STALE: human gate older than ' + staleHours + 'h — AI clock paused; not an agent hang.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
76
65
|
}
|
|
77
|
-
emit('');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 3b. research index — token economy: inject only the 5 newest rows, each
|
|
81
|
-
// truncated; the full table stays on disk for grep, sessions don't re-pay it.
|
|
82
|
-
const idx = read('docs/research/INDEX.md');
|
|
83
|
-
if (idx !== null) {
|
|
84
|
-
const rows = idx.split('\n').filter((l) => /^\|\s*\d{4}-\d{2}-\d{2}/.test(l));
|
|
85
|
-
emit(`## research/INDEX.md (${rows.length} prior investigations)`);
|
|
86
|
-
emit('grep this before any new research or debugging.');
|
|
87
|
-
for (const l of rows.slice(-5)) emit(' ' + (l.length > 220 ? l.slice(0, 217) + '…' : l));
|
|
88
|
-
if (rows.length > 5) emit(` … (+${rows.length - 5} older — grep docs/research/INDEX.md)`);
|
|
89
|
-
emit('');
|
|
90
66
|
}
|
|
67
|
+
emit('');
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
let
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let pulled = true;
|
|
102
|
-
try { execSync(`git -C "${kbPath}" pull --ff-only --quiet`, { stdio: 'ignore' }); } catch (_) { pulled = false; }
|
|
103
|
-
emit(pulled ? 'KB pulled: OK' : 'KB pull skipped (offline or conflict — using local copy)');
|
|
104
|
-
|
|
105
|
-
const cut = new Date(); cut.setMonth(cut.getMonth() - 6);
|
|
106
|
-
const cutoff = cut.toISOString().slice(0, 10);
|
|
107
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
108
|
-
const kbIndex = path.join(kbPath, 'INDEX.md');
|
|
109
|
-
if (exists(kbIndex)) {
|
|
110
|
-
const lines = (read(kbIndex) || '').split('\n').filter((l) => l && !l.startsWith('#'));
|
|
111
|
-
const snap = [`# KB snapshot — loaded ${today}`, `# Stale = created date older than ${cutoff}`, ''];
|
|
112
|
-
let stale = 0, total = 0;
|
|
113
|
-
for (const line of lines) {
|
|
114
|
-
if (line.includes('|')) total++;
|
|
115
|
-
const entryDate = (line.split('|')[0] || '').trim();
|
|
116
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(entryDate) && entryDate < cutoff) { snap.push('[STALE] ' + line); stale++; }
|
|
117
|
-
else snap.push(line);
|
|
118
|
-
}
|
|
119
|
-
try { fs.mkdirSync('docs', { recursive: true }); fs.writeFileSync('docs/.kb-snapshot.md', snap.join('\n') + '\n'); } catch (_) {}
|
|
120
|
-
emit(`KB snapshot loaded: ${total} entries (${stale} stale — researcher will re-research these)`);
|
|
121
|
-
emit('Snapshot at docs/.kb-snapshot.md — researcher reads this before web search.');
|
|
122
|
-
} else {
|
|
123
|
-
emit(`KB INDEX.md not found at ${kbPath} — run /store-wisdom to populate it.`);
|
|
69
|
+
if (!leanResume) {
|
|
70
|
+
const issues = read('docs/ISSUES.md');
|
|
71
|
+
let openCount = 0;
|
|
72
|
+
if (issues) {
|
|
73
|
+
const blocks = [];
|
|
74
|
+
let cur = null;
|
|
75
|
+
for (const l of issues.split('\n')) {
|
|
76
|
+
if (/^### /.test(l)) { if (cur) blocks.push(cur); cur = [l]; }
|
|
77
|
+
else if (cur) cur.push(l);
|
|
124
78
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} else {
|
|
128
|
-
emit('## Shared KB: configured but path not found (' + (cfg.kb_path || '') + ')');
|
|
129
|
-
emit('Fix kb_path in .issflow/kb-config.json (or re-run: npx create-issflow).');
|
|
130
|
-
emit('');
|
|
79
|
+
if (cur) blocks.push(cur);
|
|
80
|
+
openCount = blocks.filter((b) => b.some((l) => /- \[ \]/.test(l))).length;
|
|
131
81
|
}
|
|
82
|
+
emit('open issues: ' + openCount + ' (grep docs/ISSUES.md — bodies not injected)');
|
|
83
|
+
emit('');
|
|
132
84
|
}
|
|
133
85
|
|
|
134
|
-
|
|
135
|
-
emit('
|
|
136
|
-
emit('-
|
|
137
|
-
emit('
|
|
138
|
-
emit('
|
|
139
|
-
emit('-
|
|
140
|
-
emit('
|
|
141
|
-
emit(' PENDING — the plan needs a human sign-off via /overview first.');
|
|
142
|
-
emit('- UNDERSTAND-FIRST (rule 14): new free-text task -> brief back your understanding');
|
|
143
|
-
emit(' (goal · scope · assumptions · blast radius) and WAIT for confirm before executing.');
|
|
144
|
-
emit('- before debugging ANY error: grep ISSUES.md AND research/INDEX.md first.');
|
|
145
|
-
emit('- debug attempts: WARN at 2; cap 3. AUTO: log + park the slice + continue (batched');
|
|
146
|
-
emit(' report at the phase boundary). GUIDED: stop and ask you.');
|
|
147
|
-
emit('- end of every phase: run /synthesize, then /clear.');
|
|
148
|
-
emit('- small obvious change? use /quick, not /phase.');
|
|
149
|
-
emit('- token economy: keep context lean. Delegate noisy work to subagents (they');
|
|
150
|
-
emit(' return summaries). If one phase grows past ~50% of the model window, split');
|
|
151
|
-
emit(' it or /synthesize -> /clear — do not coast to auto-compact.');
|
|
152
|
-
if (kbActive) {
|
|
153
|
-
emit('- KB active: researcher checks docs/.kb-snapshot.md before web search.');
|
|
154
|
-
emit('- learned something worth keeping? run /store-wisdom.');
|
|
155
|
-
}
|
|
156
|
-
emit('=== END AUTO-CONTEXT ===');
|
|
86
|
+
emit('## RULES');
|
|
87
|
+
emit('- PM-mode: report ETA/stage/DONE|BLOCKED|WAITING|SLIP|HANDOFF — do not pair as junior dev.');
|
|
88
|
+
emit('- Plan PENDING → no source edits (rule 13). Design-direction locked before UI propose.');
|
|
89
|
+
emit('- Past agent_eta while AGENT_ACTIVE → SLIP report. Waiting human → eta_paused.');
|
|
90
|
+
emit('- Assumption budget per phase (flow-config). End phase: /synthesize (keep delivery:) → /clear.');
|
|
91
|
+
emit('- Prefer clear+handoff over compact near context gate.');
|
|
92
|
+
emit('=== END Tier-A ===');
|
|
157
93
|
|
|
158
94
|
process.stdout.write(out.join('\n') + '\n');
|
|
@@ -200,14 +200,28 @@ those gates, never the plan, the money, or the design/arch frame.
|
|
|
200
200
|
|
|
201
201
|
| House role | Kit surface |
|
|
202
202
|
|------------|-------------|
|
|
203
|
-
| BA / PM | `/overview` grill · `grill-me` · PLAN / PROPOSAL / CR gates |
|
|
204
|
-
| UX / UI | `/design`
|
|
205
|
-
| Architect | `/design`
|
|
206
|
-
| Developer | `implementer` · `/phase` · `/feature` · `/quick` |
|
|
203
|
+
| BA / PM | `/overview` grill · `grill-me` · PLAN / PROPOSAL / CR gates · delivery ETA board |
|
|
204
|
+
| UX / UI | Direction Picker → tokens · `/design` UX pack · `ui-ux-pro-max` (only if unlocked) · `ux-design` · `/ui-audit` |
|
|
205
|
+
| Architect / SA | `/design` ARCHITECTURE + auth decision · `researcher` DESIGN · `/log-decision` |
|
|
206
|
+
| Developer | `implementer` · `/phase` · `/feature` · `/quick` — **no direct user chat** |
|
|
207
207
|
| QA | `test-author` · `e2e-runner` · `/qa-audit` · `/uat` |
|
|
208
208
|
| DevOps | final-phase deploy · `/release` promote |
|
|
209
209
|
| Support | `/support` · `/log-issue` · `/runbook` · release hypercare |
|
|
210
210
|
|
|
211
|
+
**Human conversation mode:** the owner talks to the orchestrator as PM/SA. Subagents
|
|
212
|
+
never address the user. During AUTO build, report stage / ETA / WAITING / SLIP /
|
|
213
|
+
DONE — do not pair-program unless GUIDED.
|
|
214
|
+
|
|
215
|
+
### When `grill-me` runs
|
|
216
|
+
|
|
217
|
+
| Command | Grill? |
|
|
218
|
+
|---------|--------|
|
|
219
|
+
| `/overview` | Yes — automatic double grill |
|
|
220
|
+
| `/design` | Short grill only if auth/surfaces still open |
|
|
221
|
+
| `/change-request` | No full grill — 1–3 mini-asks if UX/arch/auth hit |
|
|
222
|
+
| `/phase` · `/feature` build | No — use STATE + stuck/ETA |
|
|
223
|
+
| User says “grill me” | On demand anytime |
|
|
224
|
+
|
|
211
225
|
-----
|
|
212
226
|
|
|
213
227
|
## Feature lane (hands-off delivery — `/feature`)
|
|
@@ -341,9 +355,26 @@ The orchestrator ROUTES. It does not implement or debug.
|
|
|
341
355
|
| synthesizer | `haiku` | mechanical compression — cheapest tier |
|
|
342
356
|
|
|
343
357
|
Owner wants a SPECIFIC model? Edit the role's `model:` line — values `haiku` ·
|
|
344
|
-
`sonnet` · `opus` · `inherit` · or a full model id.
|
|
345
|
-
|
|
346
|
-
|
|
358
|
+
`sonnet` · `opus` · `inherit` · or a full model id. Cross-host suggestions live in
|
|
359
|
+
`.issflow/models/MODEL-MATRIX.md` (update `as_of` when vendors change). Adaptive
|
|
360
|
+
router under `.issflow/routing/` is advisory/fail-closed — never invent unverified
|
|
361
|
+
API IDs. Hosts without per-agent model support run the session model (graceful degrade).
|
|
362
|
+
|
|
363
|
+
### Delivery clocks (finish-date honesty)
|
|
364
|
+
|
|
365
|
+
Three clocks — do not mix:
|
|
366
|
+
|
|
367
|
+
1. **Scope effort (A)** — ideal-days for commercial value / pricing
|
|
368
|
+
2. **Agent-active ETA (B)** — when AI may finish DoD if not waiting on humans
|
|
369
|
+
3. **Waiting-on-human (C)** — pauses B; `WAITING_STALE` after `staleAfterHours`
|
|
370
|
+
|
|
371
|
+
STATE `delivery:` is mandatory during `/phase` and `/feature`. Past `agent_eta`
|
|
372
|
+
while `AGENT_ACTIVE` → SLIP report. Assumption budget per phase from flow-config.
|
|
373
|
+
|
|
374
|
+
### Plan-Ready (before rule 13 approve)
|
|
375
|
+
|
|
376
|
+
Testable acceptance per phase · no TBD on auth/money/PII/primary surfaces ·
|
|
377
|
+
direction locked if UI · size+lane tags · dependencies clear.
|
|
347
378
|
|
|
348
379
|
-----
|
|
349
380
|
|
|
@@ -448,15 +479,18 @@ inject context, the model performs them itself.
|
|
|
448
479
|
|
|
449
480
|
### SESSION-OPEN (start / clear / compact-resume)
|
|
450
481
|
|
|
451
|
-
|
|
452
|
-
1. git
|
|
453
|
-
2. `docs/STATE.md` —
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
482
|
+
**Tier A (default — keep short):** SessionStart hook / model-run checklist:
|
|
483
|
+
1. git one-liner (branch · dirty count · tip).
|
|
484
|
+
2. `docs/STATE.md` top — phase, plan, design-direction, **delivery ETA/status**.
|
|
485
|
+
3. open-issue count (not full dump).
|
|
486
|
+
4. If `WAITING_STALE` / past `agent_eta` while AGENT_ACTIVE → surface that first.
|
|
487
|
+
5. Hard-rule one-liner (plan PENDING → no source edits).
|
|
488
|
+
|
|
489
|
+
**Tier B (full, on demand / first overview):** research INDEX · infra/auth · KB
|
|
490
|
+
snapshot · sprint burndown. Prefer Hard handoff (`/synthesize` keep `delivery:` →
|
|
491
|
+
`/clear`) over cold `/compact` near the context gate.
|
|
492
|
+
|
|
493
|
+
Hosts without hooks: the orchestrator runs Tier A by hand before any Edit/Write.
|
|
460
494
|
|
|
461
495
|
### SPRINT-STANDUP (auto — at phase close inside an active sprint)
|
|
462
496
|
|
|
@@ -475,9 +509,15 @@ recover: current phase, next action, open blocker.
|
|
|
475
509
|
|
|
476
510
|
The cheapest token is the one never loaded. The kit is built to minimise context:
|
|
477
511
|
|
|
512
|
+
- **SessionStart is Tier A** — git one-liner + STATE top + open-issue count +
|
|
513
|
+
delivery ETA/WAITING nudge. Target a small inject; resume/compact stays leaner.
|
|
514
|
+
- **Near context gate:** prefer writing handoff into STATE + `/synthesize` +
|
|
515
|
+
`/clear` over `/compact` (cold compact is expensive). Soft warn ~55%; hard
|
|
516
|
+
gate ~70% (see `flow-config.json`).
|
|
478
517
|
- **Phase boundary is the primary reset.** `/synthesize -> /clear` ends every
|
|
479
518
|
phase so the next one starts with a small, fresh context instead of carrying
|
|
480
|
-
the whole history forward.
|
|
519
|
+
the whole history forward. Synthesizer must **preserve the `delivery:` block**
|
|
520
|
+
in STATE. The **sprint boundary** (`/sprint close`) is a second,
|
|
481
521
|
coarser reset — synthesize + clear there too before the next sprint plans.
|
|
482
522
|
- **Lazy, not always-on.** This methodology + the skills load on demand; only the
|
|
483
523
|
SessionStart hook output is paid every session, and it injects just the live
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Model matrix (guidance)
|
|
2
|
+
|
|
3
|
+
**as_of:** 2026-11-04
|
|
4
|
+
Update this file when vendors ship new models. Do **not** hardcode unverified API IDs into agent runners.
|
|
5
|
+
|
|
6
|
+
Agent frontmatter stays: `haiku` · `sonnet` · `opus` · `inherit`.
|
|
7
|
+
Adaptive router (`.issflow/routing/`) remains advisory / fail-closed.
|
|
8
|
+
|
|
9
|
+
## Capability tiers → host suggestions
|
|
10
|
+
|
|
11
|
+
| Kit alias / role band | Claude Code | Cursor | Codex CLI | Antigravity |
|
|
12
|
+
|----------------------|-------------|--------|-----------|-------------|
|
|
13
|
+
| `haiku` (synthesizer, cheap) | Haiku 5.5 alias `haiku` | Composer 2.5 / Auto | Luna-class (plan-dependent) | Gemini 3.8 Flash |
|
|
14
|
+
| `sonnet` (researcher, e2e, daily) | Sonnet 5.5 alias `sonnet` | Composer 2.5 / Auto / Sonnet | Terra-class | Gemini 3.8 Flash |
|
|
15
|
+
| `opus` / `inherit` hard (planner, debugger, arch) | Opus 5.5 / session model | Opus / Sol / Grok flagship | Sol-class | Gemini 4 Argon |
|
|
16
|
+
| Long-horizon session | Fable / `opus[1m]` | Grok flagship | Sol + high effort | Argon High |
|
|
17
|
+
|
|
18
|
+
## Role defaults (unchanged aliases)
|
|
19
|
+
|
|
20
|
+
| Role | `model:` |
|
|
21
|
+
|------|----------|
|
|
22
|
+
| synthesizer | `haiku` |
|
|
23
|
+
| researcher · e2e-runner | `sonnet` |
|
|
24
|
+
| planner · debugger · implementer · test-author | `inherit` |
|
|
25
|
+
|
|
26
|
+
## Verify before pinning IDs
|
|
27
|
+
|
|
28
|
+
- Claude: https://code.claude.com/docs/en/model-config
|
|
29
|
+
- Cursor: https://cursor.com/help/models-and-usage/available-models
|
|
30
|
+
- Codex: https://learn.chatgpt.com/docs/models
|
|
31
|
+
- Antigravity: https://antigravity.google/docs/models/
|
|
32
|
+
|
|
33
|
+
If a host cannot switch models per agent, run everything on the session model.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host-aware model routing. Pure decision logic; does not alter approvals,
|
|
5
|
+
* mutate host configuration, or assume UI labels are model identifiers.
|
|
6
|
+
* Hosts must inject verified capabilities and a switchModel implementation.
|
|
7
|
+
*/
|
|
8
|
+
const policy = require('./policy.json');
|
|
9
|
+
const SUPPORTED = Object.freeze(['codex', 'claude', 'cursor']);
|
|
10
|
+
const TIERS = Object.freeze(['economy', 'balanced', 'advanced']);
|
|
11
|
+
const HIGH_RISK = new Set(['security', 'authentication', 'authorization', 'payments', 'secrets', 'production', 'migration']);
|
|
12
|
+
|
|
13
|
+
function route(input = {}) {
|
|
14
|
+
const { provider, agent = 'orchestrator', task = '', risk = 'low', complexity = 'medium',
|
|
15
|
+
mode = policy.defaultMode, capabilities = {} } = input;
|
|
16
|
+
if (!SUPPORTED.includes(provider)) throw new Error('Unsupported provider: ' + String(provider));
|
|
17
|
+
if (!['advisory', 'automatic'].includes(mode)) throw new Error('Invalid routing mode');
|
|
18
|
+
const elevated = risk === 'high' || HIGH_RISK.has(String(task).toLowerCase());
|
|
19
|
+
let tier = policy.agents[agent] || 'balanced';
|
|
20
|
+
if (elevated || complexity === 'high') tier = 'advanced';
|
|
21
|
+
if (!TIERS.includes(tier)) tier = 'balanced';
|
|
22
|
+
|
|
23
|
+
const preference = policy.providers[provider][tier];
|
|
24
|
+
const verified = Array.isArray(capabilities.models) ? capabilities.models : [];
|
|
25
|
+
// Never invent model IDs: only an explicitly configured, verified model is selectable.
|
|
26
|
+
const configured = capabilities.tiers && capabilities.tiers[tier];
|
|
27
|
+
const modelId = configured && configured.modelId || preference.modelId;
|
|
28
|
+
const match = verified.find(m => m.id === modelId);
|
|
29
|
+
const requestedEffort = configured && configured.reasoningEffort !== undefined
|
|
30
|
+
? configured.reasoningEffort : preference.reasoningEffort;
|
|
31
|
+
const supportedEfforts = match && Array.isArray(match.reasoningEfforts) ? match.reasoningEfforts : [];
|
|
32
|
+
const effort = requestedEffort && supportedEfforts.includes(requestedEffort) ? requestedEffort : null;
|
|
33
|
+
const eligible = !!match && (!requestedEffort || !!effort);
|
|
34
|
+
return {
|
|
35
|
+
provider, agent, tier, mode, modelId: eligible ? modelId : null,
|
|
36
|
+
reasoningEffort: eligible ? effort : null,
|
|
37
|
+
availability: eligible ? 'verified' : 'unknown',
|
|
38
|
+
action: eligible ? (mode === 'automatic' ? 'switch-request' : 'recommend') : 'inherit',
|
|
39
|
+
reason: elevated ? 'Elevated risk' : complexity === 'high' ? 'High complexity' : 'Agent default',
|
|
40
|
+
fallback: 'inherit',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Automatic mode is opt-in. switchModel must be a host-provided function
|
|
46
|
+
* returning {confirmed:true, modelId, reasoningEffort}; a request alone is
|
|
47
|
+
* never considered a successful model switch.
|
|
48
|
+
*/
|
|
49
|
+
async function execute(input = {}, host = {}) {
|
|
50
|
+
const decision = route(input);
|
|
51
|
+
if (decision.action !== 'switch-request') return { ...decision, switched: false };
|
|
52
|
+
if (host.supportsSwitch !== true || typeof host.switchModel !== 'function') {
|
|
53
|
+
return { ...decision, action: 'recommend', switched: false, reason: 'Host switch unavailable' };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const result = await host.switchModel({ modelId: decision.modelId, reasoningEffort: decision.reasoningEffort });
|
|
57
|
+
const confirmed = result && result.confirmed === true && result.modelId === decision.modelId &&
|
|
58
|
+
(decision.reasoningEffort === null || result.reasoningEffort === decision.reasoningEffort);
|
|
59
|
+
return { ...decision, action: confirmed ? 'switched' : 'recommend', switched: !!confirmed,
|
|
60
|
+
reason: confirmed ? decision.reason : 'Host did not confirm selected model/effort' };
|
|
61
|
+
} catch (_) {
|
|
62
|
+
return { ...decision, action: 'recommend', switched: false, reason: 'Host switch failed' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { route, execute, policy };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaultMode": "advisory",
|
|
4
|
+
"tiers": {
|
|
5
|
+
"economy": {
|
|
6
|
+
"description": "Low-risk documentation and routine checks"
|
|
7
|
+
},
|
|
8
|
+
"balanced": {
|
|
9
|
+
"description": "Implementation and test work"
|
|
10
|
+
},
|
|
11
|
+
"advanced": {
|
|
12
|
+
"description": "Architecture, security and complex debugging"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"agents": {
|
|
16
|
+
"orchestrator": "balanced",
|
|
17
|
+
"researcher": "economy",
|
|
18
|
+
"planner": "advanced",
|
|
19
|
+
"implementer": "balanced",
|
|
20
|
+
"test-author": "balanced",
|
|
21
|
+
"debugger": "balanced",
|
|
22
|
+
"e2e-runner": "economy",
|
|
23
|
+
"synthesizer": "economy"
|
|
24
|
+
},
|
|
25
|
+
"providers": {
|
|
26
|
+
"codex": {
|
|
27
|
+
"economy": {
|
|
28
|
+
"modelId": null,
|
|
29
|
+
"reasoningEffort": "medium"
|
|
30
|
+
},
|
|
31
|
+
"balanced": {
|
|
32
|
+
"modelId": null,
|
|
33
|
+
"reasoningEffort": "high"
|
|
34
|
+
},
|
|
35
|
+
"advanced": {
|
|
36
|
+
"modelId": null,
|
|
37
|
+
"reasoningEffort": "xhigh"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"claude": {
|
|
41
|
+
"economy": {
|
|
42
|
+
"modelId": null,
|
|
43
|
+
"reasoningEffort": null
|
|
44
|
+
},
|
|
45
|
+
"balanced": {
|
|
46
|
+
"modelId": null,
|
|
47
|
+
"reasoningEffort": null
|
|
48
|
+
},
|
|
49
|
+
"advanced": {
|
|
50
|
+
"modelId": null,
|
|
51
|
+
"reasoningEffort": null
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"cursor": {
|
|
55
|
+
"economy": {
|
|
56
|
+
"modelId": null,
|
|
57
|
+
"reasoningEffort": null
|
|
58
|
+
},
|
|
59
|
+
"balanced": {
|
|
60
|
+
"modelId": null,
|
|
61
|
+
"reasoningEffort": null
|
|
62
|
+
},
|
|
63
|
+
"advanced": {
|
|
64
|
+
"modelId": null,
|
|
65
|
+
"reasoningEffort": null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Repo-internal PostToolUse hook (NOT shipped by create-issflow — build.js only
|
|
3
|
+
// copies agents/commands/skills/hooks/istartsoft-flow/templates + README).
|
|
4
|
+
// When root entry shims are edited, nudge to keep AGENTS.md / host adapters aligned.
|
|
5
|
+
'use strict';
|
|
6
|
+
let data = '';
|
|
7
|
+
process.stdin.on('data', (c) => (data += c)).on('end', () => {
|
|
8
|
+
try {
|
|
9
|
+
const f = (JSON.parse(data || '{}').tool_input || {}).file_path || '';
|
|
10
|
+
if (/(^|[\\/])CLAUDE\.md$/.test(f)) {
|
|
11
|
+
process.stdout.write(JSON.stringify({
|
|
12
|
+
systemMessage: 'CLAUDE.md is a host shim — keep `@AGENTS.md` as the only SoT pointer. Sync BMAD if needed: save_artifact type=claude-md',
|
|
13
|
+
}));
|
|
14
|
+
} else if (/(^|[\\/])AGENTS\.md$/.test(f)) {
|
|
15
|
+
process.stdout.write(JSON.stringify({
|
|
16
|
+
systemMessage: 'AGENTS.md changed — keep it an index of `.issflow/istartsoft-flow/METHODOLOGY.md` (anti-drift: do not paste hard rules here).',
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
} catch (_) { /* ignore malformed hook input */ }
|
|
20
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Portable copy — prefer project `scripts/token-lint.js` after install.
|
|
3
|
+
// Same logic as repo-root scripts/token-lint.js for kit consumers.
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
const TOKENS = path.join(ROOT, 'docs', 'design', 'tokens.css');
|
|
10
|
+
const SCAN_GLOBS = ['src', 'app', 'components', 'pages', 'web', 'frontend', 'ui'];
|
|
11
|
+
const EXT = new Set(['.css', '.scss', '.tsx', '.jsx', '.vue', '.svelte', '.html']);
|
|
12
|
+
const HEX = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
|
|
13
|
+
const RGB = /\brgba?\([^)]+\)/g;
|
|
14
|
+
|
|
15
|
+
function walk(dir, out = []) {
|
|
16
|
+
if (!fs.existsSync(dir)) return out;
|
|
17
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name === '.git') continue;
|
|
19
|
+
const p = path.join(dir, e.name);
|
|
20
|
+
if (e.isDirectory()) walk(p, out);
|
|
21
|
+
else if (EXT.has(path.extname(e.name))) out.push(p);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(TOKENS)) {
|
|
27
|
+
console.log('token-lint: skip (no docs/design/tokens.css)');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tokenSrc = fs.readFileSync(TOKENS, 'utf8');
|
|
32
|
+
const allowed = new Set();
|
|
33
|
+
for (const m of tokenSrc.matchAll(HEX)) allowed.add(m[0].toLowerCase());
|
|
34
|
+
for (const m of tokenSrc.matchAll(RGB)) allowed.add(m[0].replace(/\s+/g, '').toLowerCase());
|
|
35
|
+
|
|
36
|
+
const files = SCAN_GLOBS.flatMap((d) => walk(path.join(ROOT, d)));
|
|
37
|
+
const hits = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (path.resolve(file) === path.resolve(TOKENS)) continue;
|
|
40
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
41
|
+
const rel = path.relative(ROOT, file);
|
|
42
|
+
for (const m of text.matchAll(HEX)) {
|
|
43
|
+
if (!allowed.has(m[0].toLowerCase())) hits.push(`${rel}: raw hex ${m[0]}`);
|
|
44
|
+
}
|
|
45
|
+
for (const m of text.matchAll(RGB)) {
|
|
46
|
+
const norm = m[0].replace(/\s+/g, '').toLowerCase();
|
|
47
|
+
if (!allowed.has(norm)) hits.push(`${rel}: raw color ${m[0]}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!hits.length) {
|
|
52
|
+
console.log(`token-lint: ok (${files.length} files)`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
console.error('token-lint: BLOCK — use semantic tokens from docs/design/tokens.css');
|
|
56
|
+
for (const h of hits.slice(0, 40)) console.error(' ' + h);
|
|
57
|
+
if (hits.length > 40) console.error(` … +${hits.length - 40} more`);
|
|
58
|
+
process.exit(1);
|
|
@@ -66,5 +66,10 @@ python3 .issflow/skills/ui-ux-pro-max/scripts/search.py "<query>" --stack <detec
|
|
|
66
66
|
4. Then apply `ux-design` authoring (wireframe + cookbook). Catalogue must still
|
|
67
67
|
pass: no emoji icons, tokens not raw hex in components, WCAG AA.
|
|
68
68
|
|
|
69
|
-
If UX-PACK is already **approved**,
|
|
70
|
-
change the
|
|
69
|
+
If UX-PACK is already **approved**, or STATE `design-direction:` is **locked**,
|
|
70
|
+
read the locked DIRECTION/tokens and stop — unless the human asked to change the
|
|
71
|
+
look (then re-picker + re-approve). Never re-roll styles at `/propose` appendix or
|
|
72
|
+
`/phase` CLOSE.
|
|
73
|
+
|
|
74
|
+
Direction Picker: fill `.issflow/templates/design/picker.html` placeholders only;
|
|
75
|
+
do not rewrite the template chrome.
|
|
@@ -47,6 +47,9 @@ wireframe does not have. If the design truly needs a frame the wireframe lacks,
|
|
|
47
47
|
STOP and update the wireframe baseline first (a design decision -> `log-decision`).
|
|
48
48
|
When an approved `docs/UX-PACK.md` exists, treat it as the project frame — drift
|
|
49
49
|
from the pack is a defect unless the pack is re-approved.
|
|
50
|
+
When `docs/design/tokens.css` exists, components must use those semantic tokens —
|
|
51
|
+
raw hex / off-grid spacing in UI files is a **BLOCK** (run `node scripts/token-lint.js`
|
|
52
|
+
when present). Frontend CLOSE also needs a **task check** for the primary user job.
|
|
50
53
|
|
|
51
54
|
-----
|
|
52
55
|
|
|
@@ -29,10 +29,14 @@ into concrete boundaries the implementer follows per phase.
|
|
|
29
29
|
- Stores / schemas (high level):
|
|
30
30
|
- Migrations approach:
|
|
31
31
|
|
|
32
|
-
## Auth & trust boundaries
|
|
33
|
-
-
|
|
34
|
-
-
|
|
32
|
+
## Auth & trust boundaries (decision record)
|
|
33
|
+
- Provider (e.g. Clerk / Auth.js / Supabase / custom / LINE Login):
|
|
34
|
+
- Session model (cookie session / JWT / opaque):
|
|
35
|
+
- Identity linking (e.g. LINE userId ↔ app user):
|
|
36
|
+
- Roles / authz:
|
|
37
|
+
- Secrets handling:
|
|
35
38
|
- Threat notes (STRIDE if security-touching):
|
|
39
|
+
- Open auth questions: none | (list — blocks Architectural approve if material)
|
|
36
40
|
|
|
37
41
|
## Integrations
|
|
38
42
|
| Service | Use | Failure mode |
|