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.
Files changed (34) hide show
  1. package/bin/cli.js +37 -12
  2. package/package.json +1 -1
  3. package/template/.issflow/README.md +4 -1
  4. package/template/.issflow/agents/planner.md +6 -1
  5. package/template/.issflow/agents/synthesizer.md +6 -4
  6. package/template/.issflow/commands/change-request.md +10 -2
  7. package/template/.issflow/commands/design.md +35 -20
  8. package/template/.issflow/commands/feature.md +6 -1
  9. package/template/.issflow/commands/overview.md +46 -7
  10. package/template/.issflow/commands/phase.md +50 -13
  11. package/template/.issflow/commands/propose.md +25 -7
  12. package/template/.issflow/commands/quick.md +2 -0
  13. package/template/.issflow/flow-config.json +11 -4
  14. package/template/.issflow/hooks/session-start.js +64 -128
  15. package/template/.issflow/istartsoft-flow/METHODOLOGY.md +57 -17
  16. package/template/.issflow/models/MODEL-MATRIX.md +33 -0
  17. package/template/.issflow/routing/model-router.js +66 -0
  18. package/template/.issflow/routing/policy.json +69 -0
  19. package/template/.issflow/scripts/entry-notice.js +20 -0
  20. package/template/.issflow/scripts/token-lint.js +58 -0
  21. package/template/.issflow/skills/ui-ux-pro-max/SKILL.md +7 -2
  22. package/template/.issflow/skills/ux-design/SKILL.md +3 -0
  23. package/template/.issflow/templates/ARCHITECTURE-template.md +7 -3
  24. package/template/.issflow/templates/PLAN-SCOPED.md +12 -0
  25. package/template/.issflow/templates/UX-PACK-template.md +19 -8
  26. package/template/.issflow/templates/design/DIRECTION.md +22 -0
  27. package/template/.issflow/templates/design/inventory.md +18 -0
  28. package/template/.issflow/templates/design/picker.html +165 -0
  29. package/template/.issflow/templates/design/platforms/line-oa.md +16 -0
  30. package/template/.issflow/templates/design/platforms/mobile-app.md +10 -0
  31. package/template/.issflow/templates/design/platforms/pwa.md +11 -0
  32. package/template/.issflow/templates/design/platforms/web-responsive.md +9 -0
  33. package/template/.issflow/templates/design/screens/SCREEN.md +19 -0
  34. package/template/.issflow/templates/design/tokens.css +47 -0
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // SessionStart hook. stdout is injected into the agent's context every session.
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
- emit('=== iStartSoftFlow AUTO-CONTEXT (injected by hook, NOT optional) ===');
19
- emit('');
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
- // 1. git state
22
- emit('## Git');
23
- emit('branch: ' + (sh('git branch --show-current').trim() || 'n/a'));
24
- const uncommitted = sh('git status --short').split('\n').filter((l) => l.trim() !== '').length;
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 !== null) {
32
- emit('## STATE.md (current position - READ THIS FIRST)');
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.md missing -> run /overview to bootstrap the project.');
39
- emit('');
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
- // 2b. active sprint (sprint layer) — surface goal + burndown if one is active
43
- const sprintMatch = (state || '').match(/^\s*sprint:\s*(\d+)\s*\(active\)/m);
44
- if (sprintMatch) {
45
- const sf = read(`docs/sprints/sprint-${sprintMatch[1]}.md`);
46
- if (sf !== null) {
47
- emit(`## Sprint ${sprintMatch[1]} (active)`);
48
- const goal = (sf.match(/goal:\s*(.+)/i) || [])[1];
49
- if (goal) emit('goal: ' + goal.trim());
50
- const burn = sf.split('\n').find((l) => /burndown|remaining|pts? left|[▁▂▃▄▅▆▇█]/i.test(l));
51
- if (burn) emit('burndown: ' + burn.trim());
52
- emit(`see docs/sprints/sprint-${sprintMatch[1]}.md for the full sprint.`);
53
- emit('');
54
- }
55
- }
56
-
57
- // 3. issue log — inject only OPEN issues (resolved ones stay in the file for
58
- // grep, but are NOT re-paid in tokens every session). Capped.
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
- // 3d. shared KB — pull latest + load snapshot
93
- const KB_CONFIG = '.issflow/kb-config.json';
94
- let kbActive = false;
95
- if (exists(KB_CONFIG)) {
96
- let cfg = {}; try { cfg = JSON.parse(read(KB_CONFIG) || '{}'); } catch (_) {}
97
- let kbPath = cfg.kb_path || '';
98
- if (kbPath.startsWith('~')) kbPath = path.join(os.homedir(), kbPath.slice(1));
99
- if (kbPath && exists(kbPath) && fs.statSync(kbPath).isDirectory()) {
100
- emit('## Shared KB');
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
- emit('');
126
- kbActive = true;
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
- // 4. hard rule reminder
135
- emit('## RULES (enforced this session)');
136
- emit('- AUTO mode (default) governs the DEV loop: follow the plan — decide + log +');
137
- emit(' continue, do NOT stop to ask. (Planning / grill still asks — that part is fine.)');
138
- emit(' Hard-stops only: security / irreversible-or-outbound actions / contradictory spec.');
139
- emit('- caveman ULTRA mode is active.');
140
- emit('- PLAN-APPROVAL gate (rule 13): no /phase or /sprint while STATE `plan:` reads');
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` (UX pack) · `ui-ux-pro-max` (look) · `ux-design` (frame + gate) · `/ui-audit` · rule 9 CLOSE |
205
- | Architect | `/design` (ARCHITECTURE) · `researcher` DESIGN · `code-standards` · `/log-decision` |
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. The installer is
345
- non-destructive, so your pins survive kit updates. Hosts without per-agent model
346
- support run everything on the session model (graceful degrade).
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
- At the start of every session, before any other work, surface:
452
- 1. git state (branch, uncommitted count, last 3 commits).
453
- 2. `docs/STATE.md` — the current position. READ THIS FIRST. If a sprint is active,
454
- surface its goal + burndown from `docs/sprints/sprint-<n>.md`.
455
- 3. open items in `docs/ISSUES.md`.
456
- 4. `docs/research/INDEX.md` (research map) + infra/auth status.
457
- 5. shared KB: pull latest + load `docs/.kb-snapshot.md` if `.issflow/kb-config.json`
458
- exists.
459
- 6. a one-line reminder of the hard rules below.
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. The **sprint boundary** (`/sprint close`) is a second,
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**, read it and stop — unless the human asked to
70
- change the look.
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
- - Authn / authz:
34
- - Secrets:
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 |