@wazir-dev/cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -10
- package/README.md +15 -15
- package/assets/demo.cast +47 -0
- package/assets/demo.gif +0 -0
- package/docs/anti-patterns/AP-23-skipping-enabled-workflows.md +28 -0
- package/docs/anti-patterns/AP-24-clarifier-deciding-scope.md +34 -0
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/roles-and-workflows.md +2 -0
- package/docs/concepts/why-wazir.md +59 -0
- package/docs/decisions/2026-03-19-deferred-items.md +564 -0
- package/docs/decisions/2026-03-19-enhancement-decisions.md +300 -0
- package/docs/readmes/INDEX.md +21 -5
- package/docs/readmes/features/expertise/README.md +2 -2
- package/docs/readmes/features/exports/README.md +2 -2
- package/docs/readmes/features/hooks/pre-compact-summary.md +1 -1
- package/docs/readmes/features/schemas/README.md +3 -0
- package/docs/readmes/features/skills/README.md +17 -0
- package/docs/readmes/features/skills/clarifier.md +5 -0
- package/docs/readmes/features/skills/claude-cli.md +5 -0
- package/docs/readmes/features/skills/codex-cli.md +5 -0
- package/docs/readmes/features/skills/dispatching-parallel-agents.md +5 -0
- package/docs/readmes/features/skills/executing-plans.md +5 -0
- package/docs/readmes/features/skills/executor.md +5 -0
- package/docs/readmes/features/skills/finishing-a-development-branch.md +5 -0
- package/docs/readmes/features/skills/gemini-cli.md +5 -0
- package/docs/readmes/features/skills/humanize.md +5 -0
- package/docs/readmes/features/skills/init-pipeline.md +5 -0
- package/docs/readmes/features/skills/receiving-code-review.md +5 -0
- package/docs/readmes/features/skills/requesting-code-review.md +5 -0
- package/docs/readmes/features/skills/reviewer.md +5 -0
- package/docs/readmes/features/skills/subagent-driven-development.md +5 -0
- package/docs/readmes/features/skills/using-git-worktrees.md +5 -0
- package/docs/readmes/features/skills/wazir.md +5 -0
- package/docs/readmes/features/skills/writing-skills.md +5 -0
- package/docs/readmes/features/workflows/prepare-next.md +1 -1
- package/docs/reference/configuration-reference.md +47 -6
- package/docs/reference/hooks.md +1 -0
- package/docs/reference/launch-checklist.md +4 -4
- package/docs/reference/review-loop-pattern.md +119 -9
- package/docs/reference/roles-reference.md +1 -0
- package/docs/reference/skill-tiers.md +147 -0
- package/docs/reference/tooling-cli.md +3 -1
- package/docs/truth-claims.yaml +12 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +214 -1
- package/exports/hosts/claude/.claude/commands/plan-review.md +3 -1
- package/exports/hosts/claude/.claude/commands/verify.md +30 -1
- package/exports/hosts/claude/.claude/settings.json +9 -0
- package/exports/hosts/claude/CLAUDE.md +1 -1
- package/exports/hosts/claude/export.manifest.json +6 -4
- package/exports/hosts/claude/host-package.json +3 -1
- package/exports/hosts/codex/AGENTS.md +1 -1
- package/exports/hosts/codex/export.manifest.json +6 -4
- package/exports/hosts/codex/host-package.json +3 -1
- package/exports/hosts/cursor/.cursor/hooks.json +4 -0
- package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +1 -1
- package/exports/hosts/cursor/export.manifest.json +6 -4
- package/exports/hosts/cursor/host-package.json +3 -1
- package/exports/hosts/gemini/GEMINI.md +1 -1
- package/exports/hosts/gemini/export.manifest.json +6 -4
- package/exports/hosts/gemini/host-package.json +3 -1
- package/hooks/context-mode-router +191 -0
- package/hooks/definitions/context_mode_router.yaml +19 -0
- package/hooks/hooks.json +31 -6
- package/hooks/protected-path-write-guard +8 -0
- package/hooks/routing-matrix.json +45 -0
- package/hooks/session-start +62 -1
- package/llms-full.txt +937 -134
- package/package.json +2 -4
- package/schemas/hook.schema.json +2 -1
- package/schemas/phase-report.schema.json +89 -0
- package/schemas/usage.schema.json +25 -1
- package/schemas/wazir-manifest.schema.json +19 -0
- package/skills/brainstorming/SKILL.md +32 -157
- package/skills/clarifier/SKILL.md +289 -111
- package/skills/claude-cli/SKILL.md +320 -0
- package/skills/codex-cli/SKILL.md +260 -0
- package/skills/debugging/SKILL.md +13 -0
- package/skills/design/SKILL.md +13 -0
- package/skills/dispatching-parallel-agents/SKILL.md +13 -0
- package/skills/executing-plans/SKILL.md +13 -0
- package/skills/executor/SKILL.md +139 -19
- package/skills/finishing-a-development-branch/SKILL.md +13 -0
- package/skills/gemini-cli/SKILL.md +260 -0
- package/skills/humanize/SKILL.md +13 -0
- package/skills/init-pipeline/SKILL.md +72 -164
- package/skills/prepare-next/SKILL.md +81 -10
- package/skills/receiving-code-review/SKILL.md +13 -0
- package/skills/requesting-code-review/SKILL.md +13 -0
- package/skills/reviewer/SKILL.md +369 -24
- package/skills/run-audit/SKILL.md +13 -0
- package/skills/scan-project/SKILL.md +13 -0
- package/skills/self-audit/SKILL.md +217 -16
- package/skills/skill-research/SKILL.md +188 -0
- package/skills/subagent-driven-development/SKILL.md +13 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -0
- package/skills/subagent-driven-development/implementer-prompt.md +8 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +7 -0
- package/skills/tdd/SKILL.md +13 -0
- package/skills/using-git-worktrees/SKILL.md +13 -0
- package/skills/using-skills/SKILL.md +13 -0
- package/skills/verification/SKILL.md +54 -3
- package/skills/wazir/SKILL.md +464 -381
- package/skills/writing-plans/SKILL.md +14 -1
- package/skills/writing-skills/SKILL.md +13 -0
- package/templates/artifacts/implementation-plan.md +3 -0
- package/templates/artifacts/tasks-template.md +133 -0
- package/templates/examples/phase-report.example.json +48 -0
- package/tooling/src/adapters/composition-engine.js +256 -0
- package/tooling/src/adapters/model-router.js +84 -0
- package/tooling/src/capture/command.js +41 -2
- package/tooling/src/capture/run-config.js +3 -1
- package/tooling/src/capture/store.js +56 -0
- package/tooling/src/capture/usage.js +106 -0
- package/tooling/src/capture/user-input.js +66 -0
- package/tooling/src/checks/ac-matrix.js +256 -0
- package/tooling/src/checks/command-registry.js +12 -0
- package/tooling/src/checks/docs-truth.js +1 -1
- package/tooling/src/checks/security-sensitivity.js +69 -0
- package/tooling/src/checks/skills.js +111 -0
- package/tooling/src/cli.js +31 -20
- package/tooling/src/commands/stats.js +161 -0
- package/tooling/src/commands/validate.js +5 -1
- package/tooling/src/export/compiler.js +33 -37
- package/tooling/src/gating/agent.js +145 -0
- package/tooling/src/guards/phase-prerequisite-guard.js +185 -0
- package/tooling/src/hooks/routing-logic.js +69 -0
- package/tooling/src/init/auto-detect.js +258 -0
- package/tooling/src/init/command.js +38 -170
- package/tooling/src/input/scanner.js +46 -0
- package/tooling/src/reports/command.js +103 -0
- package/tooling/src/reports/phase-report.js +323 -0
- package/tooling/src/state/command.js +160 -0
- package/tooling/src/state/db.js +287 -0
- package/tooling/src/status/command.js +58 -1
- package/tooling/src/verify/proof-collector.js +299 -0
- package/wazir.manifest.yaml +26 -14
- package/workflows/plan-review.md +3 -1
- package/workflows/verify.md +30 -1
|
@@ -5,6 +5,19 @@ description: Use after clarification, research, and design approval to create an
|
|
|
5
5
|
|
|
6
6
|
# Writing Plans
|
|
7
7
|
|
|
8
|
+
## Command Routing
|
|
9
|
+
Follow the Canonical Command Matrix in `hooks/routing-matrix.json`.
|
|
10
|
+
- Large commands (test runners, builds, diffs, dependency trees, linting) → context-mode tools
|
|
11
|
+
- Small commands (git status, ls, pwd, wazir CLI) → native Bash
|
|
12
|
+
- If context-mode unavailable, fall back to native Bash with warning
|
|
13
|
+
|
|
14
|
+
## Codebase Exploration
|
|
15
|
+
1. Query `wazir index search-symbols <query>` first
|
|
16
|
+
2. Use `wazir recall file <path> --tier L1` for targeted reads
|
|
17
|
+
3. Fall back to direct file reads ONLY for files identified by index queries
|
|
18
|
+
4. Maximum 10 direct file reads without a justifying index query
|
|
19
|
+
5. If no index exists: `wazir index build && wazir index summarize --tier all`
|
|
20
|
+
|
|
8
21
|
Inputs:
|
|
9
22
|
|
|
10
23
|
- approved design or approved clarified direction
|
|
@@ -34,7 +47,7 @@ Rules:
|
|
|
34
47
|
|
|
35
48
|
## Plan Review Loop
|
|
36
49
|
|
|
37
|
-
After writing the plan,
|
|
50
|
+
After writing the plan, invoke `wz:reviewer --mode plan-review` to run the plan-review loop using plan dimensions (see `workflows/plan-review.md` and `docs/reference/review-loop-pattern.md`). Do NOT call `codex exec` or `codex review` directly — the reviewer skill handles Codex integration internally.
|
|
38
51
|
|
|
39
52
|
The planner resolves findings from each pass. The loop runs for `pass_counts[depth]` passes (quick=3, standard=5, deep=7). No extension.
|
|
40
53
|
|
|
@@ -5,6 +5,19 @@ description: Use when creating new skills, editing existing skills, or verifying
|
|
|
5
5
|
|
|
6
6
|
# Writing Skills
|
|
7
7
|
|
|
8
|
+
## Command Routing
|
|
9
|
+
Follow the Canonical Command Matrix in `hooks/routing-matrix.json`.
|
|
10
|
+
- Large commands (test runners, builds, diffs, dependency trees, linting) → context-mode tools
|
|
11
|
+
- Small commands (git status, ls, pwd, wazir CLI) → native Bash
|
|
12
|
+
- If context-mode unavailable, fall back to native Bash with warning
|
|
13
|
+
|
|
14
|
+
## Codebase Exploration
|
|
15
|
+
1. Query `wazir index search-symbols <query>` first
|
|
16
|
+
2. Use `wazir recall file <path> --tier L1` for targeted reads
|
|
17
|
+
3. Fall back to direct file reads ONLY for files identified by index queries
|
|
18
|
+
4. Maximum 10 direct file reads without a justifying index query
|
|
19
|
+
5. If no index exists: `wazir index build && wazir index summarize --tier all`
|
|
20
|
+
|
|
8
21
|
## Overview
|
|
9
22
|
|
|
10
23
|
**Writing skills IS Test-Driven Development applied to process documentation.**
|
|
@@ -16,6 +16,9 @@ approval_status: required
|
|
|
16
16
|
|
|
17
17
|
## Tasks And Subtasks
|
|
18
18
|
|
|
19
|
+
Use the spec-kit task format defined in `templates/artifacts/tasks-template.md`.
|
|
20
|
+
The execution plan produced by `wz:writing-plans` follows this template.
|
|
21
|
+
|
|
19
22
|
## Acceptance Criteria
|
|
20
23
|
|
|
21
24
|
## Verification
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
artifact_type: execution_plan
|
|
3
|
+
phase: plan
|
|
4
|
+
role: planner
|
|
5
|
+
run_id: <run-id>
|
|
6
|
+
loop: 1
|
|
7
|
+
status: draft
|
|
8
|
+
sources:
|
|
9
|
+
- <approved-spec>
|
|
10
|
+
- <approved-design>
|
|
11
|
+
approval_status: required
|
|
12
|
+
template_ref: tasks-template
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Execution Plan: <Project Title>
|
|
16
|
+
|
|
17
|
+
## Constitution Check
|
|
18
|
+
- Approved spec: `<path-to-spec>`
|
|
19
|
+
- Approved design: `<path-to-design>`
|
|
20
|
+
- Branch: `<branch-name>`
|
|
21
|
+
- Depth: <quick|standard|deep>
|
|
22
|
+
|
|
23
|
+
## MVP Strategy
|
|
24
|
+
1. Complete Phase 1 (Setup) + Phase 2 (Foundational)
|
|
25
|
+
2. Complete first User Story phase → test independently → deploy/demo (MVP!)
|
|
26
|
+
3. Add stories incrementally — each adds value without breaking previous
|
|
27
|
+
|
|
28
|
+
## Dependency Graph
|
|
29
|
+
```
|
|
30
|
+
<task-id> → <task-id> (describe dependency)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Phase 1: Setup
|
|
36
|
+
|
|
37
|
+
**Goal:** Project scaffolding — directories, configs, stubs.
|
|
38
|
+
|
|
39
|
+
- [ ] T001 Setup task description with `path/to/file`
|
|
40
|
+
- [ ] T002 [P] Parallel setup task with `another/path`
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Phase 2: Foundational
|
|
45
|
+
|
|
46
|
+
**Goal:** Core infrastructure that BLOCKS all user stories. Must complete before any story phase.
|
|
47
|
+
|
|
48
|
+
- [ ] T003 Foundational task with `path/to/file`
|
|
49
|
+
- [ ] T004 [P] Parallel foundational task with `another/file`
|
|
50
|
+
|
|
51
|
+
**Independent test:** Describe how to verify this phase independently.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Phase 3: User Story 1 — [US1] <Story Title>
|
|
56
|
+
|
|
57
|
+
**Goal:** <What this story delivers to the user>
|
|
58
|
+
|
|
59
|
+
**Independent test criteria:**
|
|
60
|
+
- <How to verify this story works end-to-end without other stories>
|
|
61
|
+
|
|
62
|
+
**Implementation tasks:**
|
|
63
|
+
- [ ] T005 [US1] Task description with `path/to/file`
|
|
64
|
+
- [ ] T006 [P] [US1] Parallel task with `another/file`
|
|
65
|
+
- [ ] T007 [US1] Task description with `path/to/file`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Phase 4: User Story 2 — [US2] <Story Title>
|
|
70
|
+
|
|
71
|
+
**Goal:** <What this story delivers>
|
|
72
|
+
|
|
73
|
+
**Independent test criteria:**
|
|
74
|
+
- <Verification approach>
|
|
75
|
+
|
|
76
|
+
**Implementation tasks:**
|
|
77
|
+
- [ ] T008 [US2] Task with `path/to/file`
|
|
78
|
+
- [ ] T009 [P] [US2] Parallel task with `path/to/file`
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Phase N: Polish & Cross-Cutting
|
|
83
|
+
|
|
84
|
+
**Goal:** Final integration, documentation, cleanup.
|
|
85
|
+
|
|
86
|
+
- [ ] T0XX Polish task with `path/to/file`
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Cross-cutting Constraints
|
|
91
|
+
|
|
92
|
+
| ID | Constraint | When verified |
|
|
93
|
+
|----|-----------|--------------|
|
|
94
|
+
| CC1 | <constraint> | <timing> |
|
|
95
|
+
|
|
96
|
+
## Task Summary
|
|
97
|
+
|
|
98
|
+
| Phase | Tasks | Stories | Size | Execution |
|
|
99
|
+
|-------|-------|---------|------|-----------|
|
|
100
|
+
| 1: Setup | T001-T002 | — | S | Serial |
|
|
101
|
+
| 2: Foundational | T003-T004 | — | M | T003∥T004 |
|
|
102
|
+
| 3: [US1] | T005-T007 | US1 | M | Serial |
|
|
103
|
+
| 4: [US2] | T008-T009 | US2 | M | T008→T009 |
|
|
104
|
+
| N: Polish | T0XX | — | S | Serial |
|
|
105
|
+
|
|
106
|
+
## Format Reference
|
|
107
|
+
|
|
108
|
+
**Task line format:**
|
|
109
|
+
```
|
|
110
|
+
- [ ] [TaskID] [P?] [Story?] Description with `file/path`
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `- [ ]` — checkbox (always present)
|
|
114
|
+
- `TaskID` — sequential: T001, T002, T003...
|
|
115
|
+
- `[P]` — parallel marker: ONLY if task can run simultaneously with adjacent tasks (different files, no dependencies)
|
|
116
|
+
- `[US1]` — user story label: maps to story phases from the spec
|
|
117
|
+
- Description — clear action with exact file path in backticks
|
|
118
|
+
|
|
119
|
+
**Phase rules:**
|
|
120
|
+
- Phase 1 (Setup): project init, scaffolding
|
|
121
|
+
- Phase 2 (Foundational): BLOCKS all user stories — must complete first
|
|
122
|
+
- Phase 3+: one phase per user story, in priority order from spec
|
|
123
|
+
- Final phase: polish, cross-cutting, documentation
|
|
124
|
+
|
|
125
|
+
**Task ordering within phases:**
|
|
126
|
+
- Models before services, services before endpoints
|
|
127
|
+
- Each user story phase has: goal, independent test criteria, implementation tasks
|
|
128
|
+
- Tasks organized by user story, NOT by layer
|
|
129
|
+
|
|
130
|
+
**Parallel markers:**
|
|
131
|
+
- `[P]` means this task can run simultaneously with the next `[P]` task
|
|
132
|
+
- Only use when tasks edit different files with no shared dependencies
|
|
133
|
+
- Adjacent `[P]` tasks form a parallel group
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"phase_name": "verify",
|
|
3
|
+
"run_id": "run-2026-03-11-example",
|
|
4
|
+
"timestamp": "2026-03-11T13:00:00Z",
|
|
5
|
+
"attempted_actions": [
|
|
6
|
+
{
|
|
7
|
+
"description": "Run full test suite against implementation artifacts",
|
|
8
|
+
"outcome": "success",
|
|
9
|
+
"evidence": "42 tests passed, 0 failed"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"description": "Check lint compliance across all changed files",
|
|
13
|
+
"outcome": "success",
|
|
14
|
+
"evidence": "0 lint errors found"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"drift_analysis": {
|
|
18
|
+
"delta": 0,
|
|
19
|
+
"description": "No drift detected between spec requirements and implementation."
|
|
20
|
+
},
|
|
21
|
+
"quality_metrics": {
|
|
22
|
+
"test_pass_count": 42,
|
|
23
|
+
"test_fail_count": 0,
|
|
24
|
+
"lint_errors": 0,
|
|
25
|
+
"type_errors": 0
|
|
26
|
+
},
|
|
27
|
+
"risk_flags": [
|
|
28
|
+
{
|
|
29
|
+
"severity": "low",
|
|
30
|
+
"description": "One acceptance criterion is verified by a single unit test only.",
|
|
31
|
+
"mitigation": "Add an integration test covering the same criterion in the next phase."
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"decisions": [
|
|
35
|
+
{
|
|
36
|
+
"description": "Proceed to review without additional verification loops.",
|
|
37
|
+
"rationale": "All tests pass and drift delta is zero.",
|
|
38
|
+
"alternatives_considered": [
|
|
39
|
+
"Run a second verification loop with stricter coverage thresholds"
|
|
40
|
+
],
|
|
41
|
+
"source": "verifier"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"verdict_recommendation": {
|
|
45
|
+
"verdict": "continue",
|
|
46
|
+
"reasoning": "All quality metrics are within acceptable thresholds and no high-severity risks were identified."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { readYamlFile } from '../loaders.js';
|
|
6
|
+
import { estimateTokens } from '../capture/usage.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TOKEN_CEILING = 50_000;
|
|
9
|
+
const MODULE_CAP = 15;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve modules from the composition map for a given role and layer.
|
|
13
|
+
* Returns an array of { path, layer } objects.
|
|
14
|
+
*/
|
|
15
|
+
function resolveLayer(map, layer, role, stacks, concerns) {
|
|
16
|
+
const resolved = [];
|
|
17
|
+
|
|
18
|
+
if (layer === 'always') {
|
|
19
|
+
const entries = map.always?.[role] ?? [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
resolved.push({ path: entry, layer: 'always' });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (layer === 'auto') {
|
|
26
|
+
const allStacks = map.auto?.['all-stacks'];
|
|
27
|
+
if (allStacks) {
|
|
28
|
+
// all-roles entries apply to every role
|
|
29
|
+
const allRolesEntries = allStacks['all-roles'] ?? [];
|
|
30
|
+
for (const entry of allRolesEntries) {
|
|
31
|
+
resolved.push({ path: entry, layer: 'auto' });
|
|
32
|
+
}
|
|
33
|
+
// role-specific entries under all-stacks
|
|
34
|
+
const roleEntries = allStacks[role] ?? [];
|
|
35
|
+
for (const entry of roleEntries) {
|
|
36
|
+
resolved.push({ path: entry, layer: 'auto' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (layer === 'stacks') {
|
|
42
|
+
for (const stack of stacks) {
|
|
43
|
+
const stackDef = map.stacks?.[stack];
|
|
44
|
+
if (!stackDef) continue;
|
|
45
|
+
// executor/verifier/reviewer/etc entries for the role
|
|
46
|
+
const roleEntries = stackDef[role] ?? [];
|
|
47
|
+
for (const entry of roleEntries) {
|
|
48
|
+
resolved.push({ path: entry, layer: 'stacks' });
|
|
49
|
+
}
|
|
50
|
+
// antipatterns are included for verifier and reviewer roles
|
|
51
|
+
if (role === 'verifier' || role === 'reviewer') {
|
|
52
|
+
const antipatternEntries = stackDef.antipatterns ?? [];
|
|
53
|
+
for (const entry of antipatternEntries) {
|
|
54
|
+
resolved.push({ path: entry, layer: 'stacks' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (layer === 'concerns') {
|
|
61
|
+
for (const concern of concerns) {
|
|
62
|
+
const concernDef = map.concerns?.[concern];
|
|
63
|
+
if (!concernDef) continue;
|
|
64
|
+
const roleEntries = concernDef[role] ?? [];
|
|
65
|
+
for (const entry of roleEntries) {
|
|
66
|
+
resolved.push({ path: entry, layer: 'concerns' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Deduplicate modules, keeping first occurrence (highest priority).
|
|
76
|
+
*/
|
|
77
|
+
function deduplicateModules(modules) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const result = [];
|
|
80
|
+
for (const mod of modules) {
|
|
81
|
+
if (!seen.has(mod.path)) {
|
|
82
|
+
seen.add(mod.path);
|
|
83
|
+
result.push(mod);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compose expertise modules for a given role, stack set, and concern set.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} options
|
|
93
|
+
* @param {string} options.role - The role (executor, verifier, reviewer, etc.)
|
|
94
|
+
* @param {string[]} options.stacks - Detected project stacks (e.g. ['node', 'react'])
|
|
95
|
+
* @param {string[]} options.concerns - Declared task concerns (e.g. ['rtl', 'security-auth'])
|
|
96
|
+
* @param {string} options.projectRoot - Absolute path to the project root
|
|
97
|
+
* @param {string} options.runRoot - Absolute path to the run root for artifact output
|
|
98
|
+
* @param {string} [options.task] - Optional task identifier for the proof artifact
|
|
99
|
+
* @param {number} [options.tokenCeiling] - Max token budget (default 50,000)
|
|
100
|
+
* @returns {{ prompt: string, manifest: object }}
|
|
101
|
+
*/
|
|
102
|
+
export function composeExpertise(options) {
|
|
103
|
+
const {
|
|
104
|
+
role,
|
|
105
|
+
stacks = [],
|
|
106
|
+
concerns = [],
|
|
107
|
+
projectRoot,
|
|
108
|
+
runRoot,
|
|
109
|
+
task = 'default',
|
|
110
|
+
tokenCeiling = DEFAULT_TOKEN_CEILING,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const mapPath = path.join(projectRoot, 'expertise', 'composition-map.yaml');
|
|
114
|
+
const map = readYamlFile(mapPath);
|
|
115
|
+
const expertiseRoot = path.join(projectRoot, 'expertise');
|
|
116
|
+
|
|
117
|
+
// Resolve modules in priority order: always > auto > stacks > concerns
|
|
118
|
+
const layers = ['always', 'auto', 'stacks', 'concerns'];
|
|
119
|
+
let allModules = [];
|
|
120
|
+
for (const layer of layers) {
|
|
121
|
+
const layerModules = resolveLayer(map, layer, role, stacks, concerns);
|
|
122
|
+
allModules = allModules.concat(layerModules);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Deduplicate
|
|
126
|
+
allModules = deduplicateModules(allModules);
|
|
127
|
+
|
|
128
|
+
// Read file contents and compute tokens
|
|
129
|
+
const loaded = [];
|
|
130
|
+
const warnings = [];
|
|
131
|
+
|
|
132
|
+
for (const mod of allModules) {
|
|
133
|
+
const fullPath = path.join(expertiseRoot, mod.path);
|
|
134
|
+
try {
|
|
135
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
136
|
+
const tokens = estimateTokens(Buffer.byteLength(content, 'utf8'));
|
|
137
|
+
loaded.push({ ...mod, content, tokens, fullPath });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
warnings.push(`warning: skipping missing module ${mod.path}: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Enforce budget: 15-module cap + token ceiling
|
|
144
|
+
// Drop in reverse priority: concerns first, then stacks, then auto
|
|
145
|
+
const dropOrder = ['concerns', 'stacks', 'auto', 'always'];
|
|
146
|
+
let included = [...loaded];
|
|
147
|
+
let dropped = [];
|
|
148
|
+
|
|
149
|
+
// Enforce module cap
|
|
150
|
+
if (included.length > MODULE_CAP) {
|
|
151
|
+
const toDrop = enforceLimit(included, MODULE_CAP, dropOrder);
|
|
152
|
+
for (const m of toDrop) m.drop_reason = 'module_cap_exceeded';
|
|
153
|
+
dropped = dropped.concat(toDrop);
|
|
154
|
+
included = included.filter((m) => !toDrop.includes(m));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Enforce token ceiling
|
|
158
|
+
let totalTokens = included.reduce((sum, m) => sum + m.tokens, 0);
|
|
159
|
+
if (totalTokens > tokenCeiling) {
|
|
160
|
+
const toDrop = enforceTokenBudget(included, tokenCeiling, dropOrder);
|
|
161
|
+
for (const m of toDrop) m.drop_reason = 'token_ceiling_exceeded';
|
|
162
|
+
dropped = dropped.concat(toDrop);
|
|
163
|
+
included = included.filter((m) => !toDrop.includes(m));
|
|
164
|
+
totalTokens = included.reduce((sum, m) => sum + m.tokens, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build the combined prompt
|
|
168
|
+
const promptParts = [];
|
|
169
|
+
for (const mod of included) {
|
|
170
|
+
promptParts.push(`<!-- module: ${mod.path} (${mod.layer}) -->\n${mod.content}`);
|
|
171
|
+
}
|
|
172
|
+
const prompt = promptParts.join('\n\n---\n\n');
|
|
173
|
+
|
|
174
|
+
// Compute prompt hash
|
|
175
|
+
const promptHash = crypto.createHash('sha256').update(prompt).digest('hex');
|
|
176
|
+
|
|
177
|
+
const manifest = {
|
|
178
|
+
modules_included: included.map((m) => ({ path: m.path, layer: m.layer, tokens: m.tokens })),
|
|
179
|
+
modules_dropped: dropped.map((m) => ({ path: m.path, layer: m.layer, tokens: m.tokens, reason: m.drop_reason })),
|
|
180
|
+
total_tokens: totalTokens,
|
|
181
|
+
prompt_hash: promptHash,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Write warnings to stderr
|
|
185
|
+
for (const w of warnings) {
|
|
186
|
+
process.stderr.write(`${w}\n`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Write composition proof artifact
|
|
190
|
+
writeProofArtifact(runRoot, role, task, manifest);
|
|
191
|
+
|
|
192
|
+
return { prompt, manifest };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Enforce a maximum count by dropping modules in reverse priority order.
|
|
197
|
+
*/
|
|
198
|
+
function enforceLimit(modules, limit, dropOrder) {
|
|
199
|
+
const toDrop = [];
|
|
200
|
+
let current = modules.length;
|
|
201
|
+
|
|
202
|
+
for (const layer of dropOrder) {
|
|
203
|
+
if (current <= limit) break;
|
|
204
|
+
// Iterate in reverse to drop last-added first within a layer
|
|
205
|
+
const layerModules = modules.filter((m) => m.layer === layer);
|
|
206
|
+
for (let i = layerModules.length - 1; i >= 0; i--) {
|
|
207
|
+
if (current <= limit) break;
|
|
208
|
+
toDrop.push(layerModules[i]);
|
|
209
|
+
current--;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return toDrop;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Enforce a token ceiling by dropping modules in reverse priority order.
|
|
218
|
+
*/
|
|
219
|
+
function enforceTokenBudget(modules, ceiling, dropOrder) {
|
|
220
|
+
const toDrop = [];
|
|
221
|
+
let totalTokens = modules.reduce((sum, m) => sum + m.tokens, 0);
|
|
222
|
+
|
|
223
|
+
for (const layer of dropOrder) {
|
|
224
|
+
if (totalTokens <= ceiling) break;
|
|
225
|
+
const layerModules = modules.filter((m) => m.layer === layer && !toDrop.includes(m));
|
|
226
|
+
for (let i = layerModules.length - 1; i >= 0; i--) {
|
|
227
|
+
if (totalTokens <= ceiling) break;
|
|
228
|
+
toDrop.push(layerModules[i]);
|
|
229
|
+
totalTokens -= layerModules[i].tokens;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return toDrop;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Write the composition proof artifact to the run artifacts directory.
|
|
238
|
+
*/
|
|
239
|
+
function writeProofArtifact(runRoot, role, task, manifest) {
|
|
240
|
+
try {
|
|
241
|
+
const artifactsDir = path.join(runRoot, 'artifacts');
|
|
242
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
243
|
+
|
|
244
|
+
const artifactPath = path.join(artifactsDir, `composition-${role}-${task}.json`);
|
|
245
|
+
const artifact = {
|
|
246
|
+
generated_at: new Date().toISOString(),
|
|
247
|
+
role,
|
|
248
|
+
task,
|
|
249
|
+
...manifest,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
process.stderr.write(`warning: failed to write composition proof artifact: ${err.message}\n`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model routing table — maps task types to recommended models.
|
|
3
|
+
* @type {Object<string, {model: string, reason: string}>}
|
|
4
|
+
*/
|
|
5
|
+
const MODEL_ROUTING_TABLE = {
|
|
6
|
+
// Mechanical tasks — Haiku
|
|
7
|
+
'fetch-url': { model: 'haiku', reason: 'Mechanical, no reasoning needed' },
|
|
8
|
+
'write-handoff': { model: 'haiku', reason: 'Structured file operations' },
|
|
9
|
+
'compress-archive': { model: 'haiku', reason: 'File manipulation' },
|
|
10
|
+
|
|
11
|
+
// Comprehension tasks — Sonnet
|
|
12
|
+
'read-summarize': { model: 'sonnet', reason: 'Comprehension, not deep reasoning' },
|
|
13
|
+
'write-implementation':{ model: 'sonnet', reason: 'Good spec + plan = mechanical coding' },
|
|
14
|
+
'task-review': { model: 'sonnet', reason: 'Diff review against clear spec' },
|
|
15
|
+
'extract-learnings': { model: 'sonnet', reason: 'Structured extraction' },
|
|
16
|
+
'internal-review': { model: 'sonnet', reason: 'Pattern matching against expertise' },
|
|
17
|
+
'run-tests': { model: 'sonnet', reason: 'Test execution and analysis' },
|
|
18
|
+
|
|
19
|
+
// Judgment tasks — Opus
|
|
20
|
+
'orchestrate': { model: 'opus', reason: 'Needs judgment and coordination' },
|
|
21
|
+
'spec-harden': { model: 'opus', reason: 'Adversarial thinking required' },
|
|
22
|
+
'design': { model: 'opus', reason: 'Creativity + architecture decisions' },
|
|
23
|
+
'final-review': { model: 'opus', reason: 'Holistic judgment against original input' },
|
|
24
|
+
'brainstorm': { model: 'opus', reason: 'Creative exploration' },
|
|
25
|
+
'plan': { model: 'opus', reason: 'Strategic task decomposition' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the recommended model for a task type.
|
|
30
|
+
*
|
|
31
|
+
* If multi-model mode is not enabled, returns `{ model: 'inherit' }`.
|
|
32
|
+
* If config contains `model_overrides`, those take precedence over the
|
|
33
|
+
* default routing table. Unknown task types fall back to 'opus' (safe
|
|
34
|
+
* default — never under-model a task).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} taskType - one of the keys in MODEL_ROUTING_TABLE
|
|
37
|
+
* @param {object} config - project config (may override routing)
|
|
38
|
+
* @returns {{model: string, reason: string, overridden: boolean}}
|
|
39
|
+
*/
|
|
40
|
+
export function getModelForTask(taskType, config = {}) {
|
|
41
|
+
if (!isMultiModelEnabled(config)) {
|
|
42
|
+
return { model: 'inherit', reason: 'Multi-model mode not enabled', overridden: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check config-level overrides first
|
|
46
|
+
const overrides = config.model_overrides ?? {};
|
|
47
|
+
if (overrides[taskType]) {
|
|
48
|
+
return {
|
|
49
|
+
model: overrides[taskType].model ?? 'opus',
|
|
50
|
+
reason: overrides[taskType].reason ?? 'Config override',
|
|
51
|
+
overridden: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Look up the default routing table
|
|
56
|
+
const entry = MODEL_ROUTING_TABLE[taskType];
|
|
57
|
+
if (entry) {
|
|
58
|
+
return { model: entry.model, reason: entry.reason, overridden: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Unknown task type — safe default is opus (never under-model)
|
|
62
|
+
return { model: 'opus', reason: 'Unknown task type — safe default', overridden: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if multi-model mode is enabled.
|
|
67
|
+
* @param {object} config
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isMultiModelEnabled(config = {}) {
|
|
71
|
+
return config.model_mode === 'multi-model';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get all routing decisions for logging/stats.
|
|
76
|
+
* @returns {Object<string, {model: string, reason: string}>}
|
|
77
|
+
*/
|
|
78
|
+
export function getRoutingTable() {
|
|
79
|
+
const copy = {};
|
|
80
|
+
for (const [key, value] of Object.entries(MODEL_ROUTING_TABLE)) {
|
|
81
|
+
copy[key] = { ...value };
|
|
82
|
+
}
|
|
83
|
+
return copy;
|
|
84
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { parseCommandOptions, parsePositiveInteger } from '../command-options.js';
|
|
5
5
|
import { readYamlFile } from '../loaders.js';
|
|
6
|
+
import { validateRunCompletion } from '../guards/phase-prerequisite-guard.js';
|
|
6
7
|
import { findProjectRoot } from '../project-root.js';
|
|
7
8
|
import { resolveStateRoot } from '../state-root.js';
|
|
8
9
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { readRunConfig, getPhaseLoopCap } from './run-config.js';
|
|
20
21
|
import { readUsage, generateReport, initUsage, recordCaptureSavings, recordPhaseUsage } from './usage.js';
|
|
21
22
|
import { evaluateLoopCapGuard } from '../guards/loop-cap-guard.js';
|
|
23
|
+
import { evaluatePhasePrerequisiteGuard } from '../guards/phase-prerequisite-guard.js';
|
|
22
24
|
|
|
23
25
|
function formatResult(payload, options = {}) {
|
|
24
26
|
if (options.json) {
|
|
@@ -56,7 +58,7 @@ function resolveCaptureContext(parsed, context = {}) {
|
|
|
56
58
|
const projectRoot = findProjectRoot(context.cwd ?? process.cwd());
|
|
57
59
|
const manifest = readYamlFile(path.join(projectRoot, 'wazir.manifest.yaml'));
|
|
58
60
|
const { options } = parseCommandOptions(parsed.args, {
|
|
59
|
-
boolean: ['json'],
|
|
61
|
+
boolean: ['json', 'complete'],
|
|
60
62
|
string: [
|
|
61
63
|
'run',
|
|
62
64
|
'phase',
|
|
@@ -160,12 +162,34 @@ function handleInit(parsed, context = {}) {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
function handleEvent(parsed, context = {}) {
|
|
163
|
-
const { stateRoot, options } = resolveCaptureContext(parsed, context);
|
|
165
|
+
const { projectRoot, stateRoot, options } = resolveCaptureContext(parsed, context);
|
|
164
166
|
|
|
165
167
|
requireOption(options, 'run', 'Usage: wazir capture event --run <id> --event <name> [--phase <phase>] [--status <status>] [--loop-count <n>] [--message <text>] [--state-root <path>] [--json]');
|
|
166
168
|
requireOption(options, 'event', 'Usage: wazir capture event --run <id> --event <name> [--phase <phase>] [--status <status>] [--loop-count <n>] [--message <text>] [--state-root <path>] [--json]');
|
|
167
169
|
|
|
168
170
|
const runPaths = getRunPaths(stateRoot, options.run);
|
|
171
|
+
|
|
172
|
+
// Phase prerequisite gate — block phase_enter if prerequisites not met (exit 44)
|
|
173
|
+
if (options.event === 'phase_enter') {
|
|
174
|
+
if (!fs.existsSync(runPaths.statusPath)) {
|
|
175
|
+
// Standalone mode — allow without guard check (matches handleLoopCheck pattern)
|
|
176
|
+
} else {
|
|
177
|
+
const guardResult = evaluatePhasePrerequisiteGuard({
|
|
178
|
+
run_id: options.run,
|
|
179
|
+
phase: options.phase,
|
|
180
|
+
state_root: stateRoot,
|
|
181
|
+
project_root: projectRoot,
|
|
182
|
+
});
|
|
183
|
+
if (!guardResult.allowed) {
|
|
184
|
+
return {
|
|
185
|
+
exitCode: 44,
|
|
186
|
+
stderr: `Phase prerequisite gate failed (exit 44): ${guardResult.reason}\n`,
|
|
187
|
+
stdout: options.json ? `${JSON.stringify(guardResult, null, 2)}\n` : '',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
const status = readStatus(runPaths);
|
|
170
194
|
const event = createBaseEvent(options.event, {
|
|
171
195
|
run_id: options.run,
|
|
@@ -303,6 +327,21 @@ function handleSummary(parsed, context = {}) {
|
|
|
303
327
|
|
|
304
328
|
const runPaths = getRunPaths(stateRoot, options.run);
|
|
305
329
|
const status = readStatus(runPaths);
|
|
330
|
+
|
|
331
|
+
// Enforce workflow completion before allowing summary to finalize
|
|
332
|
+
if (options.complete) {
|
|
333
|
+
const projectRoot = findProjectRoot();
|
|
334
|
+
const manifestPath = path.join(projectRoot, 'wazir.manifest.yaml');
|
|
335
|
+
const result = validateRunCompletion(runPaths.runRoot, manifestPath);
|
|
336
|
+
if (!result.complete) {
|
|
337
|
+
const msg = `Run incomplete: ${result.missing.length} workflow(s) not finished: ${result.missing.join(', ')}`;
|
|
338
|
+
if (options.json) {
|
|
339
|
+
return { exitCode: 1, stdout: JSON.stringify({ run_id: options.run, complete: false, missing_workflows: result.missing, error: msg }, null, 2) + '\n' };
|
|
340
|
+
}
|
|
341
|
+
return { exitCode: 1, stderr: msg + '\n' };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
306
345
|
const eventName = options.event ?? 'pre_compact_summary';
|
|
307
346
|
const summaryContent = readInput();
|
|
308
347
|
const summaryPath = writeSummary(runPaths, summaryContent);
|
|
@@ -16,6 +16,8 @@ export function readRunConfig(runPaths) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function getPhaseLoopCap(runConfig, phase) {
|
|
19
|
-
|
|
19
|
+
// Support both workflow_policy (new) and phase_policy (legacy)
|
|
20
|
+
const policyMap = runConfig?.workflow_policy ?? runConfig?.phase_policy ?? {};
|
|
21
|
+
const policy = policyMap[phase] ?? DEFAULT_PHASE_POLICY;
|
|
20
22
|
return policy.loop_cap ?? DEFAULT_PHASE_POLICY.loop_cap;
|
|
21
23
|
}
|