mikoshi-construct 0.2.0 → 0.3.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/README.md +16 -1
- package/dist/cli.js +1450 -712
- package/package.json +16 -11
- package/templates/ai/claude/_claude/commands/plan.md +2 -0
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +20 -6
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +53 -24
- package/templates/base/architecture/decisions/README.md +26 -0
- package/templates/base/architecture/principles.md +3 -1
- /package/templates/presets/monorepo/{baseline → sample}/scripts/tests/lint/syntax-policy.test.ts.eta +0 -0
- /package/templates/presets/node-backend/{baseline → sample}/scripts/tests/lint/syntax-policy.test.ts +0 -0
- /package/templates/presets/node-frontend/{baseline → sample}/scripts/tests/lint/syntax-policy.test.ts +0 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikoshi-construct",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "Bootstrap for AI-native software projects. Start with a proven engineering workflow instead of an empty repository.",
|
|
6
6
|
"author": "Eli Tabrisov",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.
|
|
8
|
+
"homepage": "https://e1i.github.io/mikoshi-construct/",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/E1i/mikoshi-construct.git"
|
|
@@ -49,24 +49,29 @@
|
|
|
49
49
|
"tsup": "^8.5.1",
|
|
50
50
|
"tsx": "^4.23.13",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
|
+
"vitepress": "2.0.0-alpha.20",
|
|
52
53
|
"vitest": "^5.0.0",
|
|
53
54
|
"yaml": "^2.9.1"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "tsup",
|
|
58
|
+
"changeset": "changeset",
|
|
59
|
+
"ci": "pnpm run quality",
|
|
60
|
+
"composition:check": "tsx scripts/composition/check.ts",
|
|
61
|
+
"composition:render": "tsx scripts/composition/sync-docs.ts",
|
|
57
62
|
"dev": "tsx src/cli.ts",
|
|
63
|
+
"docs:build": "vitepress build docs",
|
|
64
|
+
"docs:dev": "vitepress dev docs",
|
|
65
|
+
"docs:preview": "vitepress preview docs",
|
|
58
66
|
"lint": "eslint .",
|
|
59
67
|
"lint:fix": "eslint --fix .",
|
|
60
|
-
"
|
|
68
|
+
"privacy:check": "tsx scripts/privacy/check.ts",
|
|
69
|
+
"quality": "pnpm composition:check && pnpm privacy:check && pnpm lint && pnpm typecheck && pnpm test",
|
|
70
|
+
"release": "pnpm build && changeset publish",
|
|
71
|
+
"release:verify": "tsx scripts/release/verify-published.ts",
|
|
61
72
|
"test": "vitest run",
|
|
62
73
|
"test:watch": "vitest",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"composition:check": "tsx scripts/composition/check.ts",
|
|
66
|
-
"privacy:check": "tsx scripts/privacy/check.ts",
|
|
67
|
-
"ci": "pnpm run quality",
|
|
68
|
-
"changeset": "changeset",
|
|
69
|
-
"version-packages": "changeset version",
|
|
70
|
-
"release": "pnpm build && changeset publish"
|
|
74
|
+
"typecheck": "tsc --noEmit",
|
|
75
|
+
"version-packages": "changeset version"
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -12,6 +12,8 @@ Rules:
|
|
|
12
12
|
- Two to six tasks. Each task is independently verifiable by the harness and leaves the tree green.
|
|
13
13
|
- Each task has two to four acceptance criteria that a harness run or a test can confirm. "Works" is
|
|
14
14
|
not a criterion; "GET /v1/things returns 200 with the `Thing` schema and the contract test passes" is.
|
|
15
|
+
- A criterion is verified by what the task changes itself. If satisfying it needs an action outside the
|
|
16
|
+
task, it belongs to that task, not this one.
|
|
15
17
|
- Order tasks so the contract and composition changes come first, then implementation, then anything
|
|
16
18
|
that consumes the new behaviour.
|
|
17
19
|
- Classify each task `low`, `medium` or `high` with the rules in `architecture/principles.md`
|
|
@@ -31,6 +31,15 @@ repository's CLAUDE.md and `construct.json`.
|
|
|
31
31
|
only when a rejected response is expected to be a transient shape error rather than a bad brief.
|
|
32
32
|
The user's `/implement` invocation is the opt-in the tool requires. Note the run identifier the
|
|
33
33
|
Workflow tool reports when it launches the run and again when it completes; step 4 records it.
|
|
34
|
+
The design step runs inside the ladder, not before it, and its outcome is one of the `attempts`
|
|
35
|
+
like any other. The statuses a run can return are:
|
|
36
|
+
- `done` — a rung passed the harness and every design step the run took completed.
|
|
37
|
+
- `degraded` — a rung passed the harness, but a design step was rejected by the schema and the
|
|
38
|
+
run continued without it. The result's `effort` is the class that actually executed.
|
|
39
|
+
- `design incomplete` — a high-effort run whose architect was rejected by the schema. No
|
|
40
|
+
implementer ran without a spec; the result carries the validator's text in `validationError`.
|
|
41
|
+
- `failed` — every rung ran and the harness stayed red; `lastFailure` carries the excerpt.
|
|
42
|
+
- `blocked` — the last rung stopped on a question; `question` carries it verbatim.
|
|
34
43
|
4. Record the run: append one JSON line to `.construct/runs.jsonl` (create the directory if needed)
|
|
35
44
|
with exactly these fields and no others:
|
|
36
45
|
- `run` — the Workflow run identifier from step 3. It is the key `construct cost` joins the entry
|
|
@@ -38,13 +47,15 @@ repository's CLAUDE.md and `construct.json`.
|
|
|
38
47
|
unjoinable, which is the truth about it.
|
|
39
48
|
- `at` — the ISO timestamp.
|
|
40
49
|
- `task` — the task text, first 120 characters.
|
|
41
|
-
- `effort` — the class
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
- `effort` — the class the run performed: the result's `effort` when it carries one, and only
|
|
51
|
+
then the class you chose in step 1. A run whose design step did not complete is never written
|
|
52
|
+
down as `high`; the result has already degraded it.
|
|
53
|
+
- `status` — the result's status verbatim, one of the five in step 3.
|
|
54
|
+
- `rung` — the effort of the rung that finished: `effort` from the result when it carries one,
|
|
44
55
|
otherwise the `effort` of the last entry in `attempts`.
|
|
45
56
|
- `attempts` — the result's `attempts` array verbatim; each entry carries its `rung`, `effort`,
|
|
46
|
-
`outcome` and the `reason` that separates an invalid response shape from a red harness from a
|
|
47
|
-
blocked report.
|
|
57
|
+
`outcome` and the `reason` that separates an invalid response shape from a red harness, from a
|
|
58
|
+
blocked report and from a design the schema rejected.
|
|
48
59
|
- `agents`, `tokens`, `toolUses`, `seconds` — the Workflow tool's own accounting for the run,
|
|
49
60
|
exactly as it reported it. Write `"unknown"` for a token figure it did not report, never `0`.
|
|
50
61
|
The ledger carries counts and reasons only — never a prompt, a response or any other message
|
|
@@ -54,7 +65,10 @@ repository's CLAUDE.md and `construct.json`.
|
|
|
54
65
|
gitignored.
|
|
55
66
|
5. Relay the result: status, the effort rung that succeeded and how many attempts it took, the
|
|
56
67
|
files changed, and the harness tail. When the status is `blocked`, put the architect's or
|
|
57
|
-
implementer's question to the user verbatim. When `failed`, give the last failure excerpt.
|
|
68
|
+
implementer's question to the user verbatim. When `failed`, give the last failure excerpt. When
|
|
69
|
+
`design incomplete`, say that the design step did not complete and give `validationError` as the
|
|
70
|
+
runtime reported it; when `degraded`, say which design step was rejected and that the reported
|
|
71
|
+
class is the one that executed, not the one that was requested.
|
|
58
72
|
Unless `construct.json` sets `report.usage` to `false`, end with one usage line for this run,
|
|
59
73
|
from the Workflow tool's own accounting: agents, subagent tokens, tool uses, wall time — so the
|
|
60
74
|
cost of the rung that succeeded is on record next to the result. When `construct` is on the PATH,
|
|
@@ -2,7 +2,7 @@ export const meta = {
|
|
|
2
2
|
name: 'implement',
|
|
3
3
|
description: 'Implement a task at low effort, verify with the harness, escalate on repeated failure or ambiguity',
|
|
4
4
|
phases: [
|
|
5
|
-
{ title: 'Design', detail: 'architect,
|
|
5
|
+
{ title: 'Design', detail: 'architect inside the run, for high effort before the first rung and after a blocked or failed attempt' },
|
|
6
6
|
{ title: 'Implement', detail: 'implementer at the current rung' },
|
|
7
7
|
{ title: 'Verify', detail: 'harness against the working tree' },
|
|
8
8
|
],
|
|
@@ -53,6 +53,8 @@ const SPEC = {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const DEFAULT_RETRY_LIMIT = 0
|
|
56
|
+
const DESIGN_EFFORT = 'xhigh'
|
|
57
|
+
const EFFORT_WITHOUT_DESIGN = { high: 'medium', xhigh: 'medium' }
|
|
56
58
|
|
|
57
59
|
const task = args.task
|
|
58
60
|
const acceptance = args.acceptance ?? []
|
|
@@ -125,22 +127,58 @@ function implementerPrompt(spec, feedback) {
|
|
|
125
127
|
|
|
126
128
|
let spec = null
|
|
127
129
|
let feedback = null
|
|
130
|
+
let designComplete = false
|
|
131
|
+
let designFailed = false
|
|
132
|
+
let designError = ''
|
|
128
133
|
const attempts = []
|
|
129
134
|
|
|
135
|
+
function performedEffort(effort) {
|
|
136
|
+
return designComplete ? effort : (EFFORT_WITHOUT_DESIGN[effort] ?? effort)
|
|
137
|
+
}
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
async function design(rung, reason, label) {
|
|
132
140
|
phase('Design')
|
|
133
|
-
|
|
141
|
+
const result = await ask(architectPrompt(reason), {
|
|
134
142
|
agentType: 'architect',
|
|
135
|
-
effort:
|
|
143
|
+
effort: DESIGN_EFFORT,
|
|
136
144
|
phase: 'Design',
|
|
137
|
-
label
|
|
145
|
+
label,
|
|
138
146
|
schema: SPEC,
|
|
139
147
|
})
|
|
148
|
+
if (result == null) {
|
|
149
|
+
designComplete = false
|
|
150
|
+
designFailed = true
|
|
151
|
+
designError = lastValidationError ?? ''
|
|
152
|
+
attempts.push({ rung, effort: DESIGN_EFFORT, outcome: 'design schema invalid', reason: designError })
|
|
153
|
+
log(`${label}: the design step did not complete`)
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
spec = result
|
|
157
|
+
designComplete = true
|
|
158
|
+
attempts.push({ rung, effort: DESIGN_EFFORT, outcome: 'designed', reason: '' })
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function designIncomplete(effort, question) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'design incomplete',
|
|
165
|
+
effort: performedEffort(effort),
|
|
166
|
+
attempts,
|
|
167
|
+
validationError: designError,
|
|
168
|
+
question: question ?? '',
|
|
169
|
+
lastFailure: feedback ?? '',
|
|
170
|
+
}
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
for (const [index, effort] of rungs.entries()) {
|
|
143
174
|
const rung = index + 1
|
|
175
|
+
|
|
176
|
+
if (rung === 1 && args.effort === 'high') {
|
|
177
|
+
const designed = await design(rung, 'The task is classified as high effort; design it before any implementation.', 'design')
|
|
178
|
+
if (!designed)
|
|
179
|
+
return designIncomplete(effort)
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
phase('Implement')
|
|
145
183
|
log(`rung ${rung}/${rungs.length} @ ${effort}: implementing`)
|
|
146
184
|
const report = await ask(implementerPrompt(spec, feedback), {
|
|
@@ -159,15 +197,11 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
159
197
|
attempts.push({ rung, effort, outcome: 'blocked', reason: report.question, question: report.question })
|
|
160
198
|
if (rung === rungs.length)
|
|
161
199
|
return { status: 'blocked', question: report.question, attempts }
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
effort
|
|
165
|
-
phase: 'Design',
|
|
166
|
-
label: `design after blocked ${rung}`,
|
|
167
|
-
schema: SPEC,
|
|
168
|
-
})
|
|
200
|
+
const designed = await design(rung, `The implementer stopped on this question:\n${report.question}`, `design after blocked ${rung}`)
|
|
201
|
+
if (!designed && args.effort === 'high')
|
|
202
|
+
return designIncomplete(effort, report.question)
|
|
169
203
|
feedback = null
|
|
170
|
-
log(`rung ${rung}/${rungs.length} @ ${effort}: blocked, architect answered`)
|
|
204
|
+
log(`rung ${rung}/${rungs.length} @ ${effort}: blocked, architect ${designed ? 'answered' : 'did not answer'}`)
|
|
171
205
|
continue
|
|
172
206
|
}
|
|
173
207
|
|
|
@@ -194,8 +228,8 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
194
228
|
|
|
195
229
|
if (passed) {
|
|
196
230
|
return {
|
|
197
|
-
status: 'done',
|
|
198
|
-
effort,
|
|
231
|
+
status: designFailed && !designComplete ? 'degraded' : 'done',
|
|
232
|
+
effort: performedEffort(effort),
|
|
199
233
|
attempts,
|
|
200
234
|
files: report.files,
|
|
201
235
|
summary: report.summary,
|
|
@@ -214,16 +248,11 @@ for (const [index, effort] of rungs.entries()) {
|
|
|
214
248
|
feedback = `Security invariant failed: ${verdict.securityFinding}\n${feedback}`
|
|
215
249
|
|
|
216
250
|
if (rung === rungs.length - 1) {
|
|
217
|
-
phase('Design')
|
|
218
251
|
log(`rung ${rung}/${rungs.length} failed twice: architect redesigns before the last rung`)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
effort
|
|
222
|
-
phase: 'Design',
|
|
223
|
-
label: 'design before last rung',
|
|
224
|
-
schema: SPEC,
|
|
225
|
-
})
|
|
252
|
+
const designed = await design(rung, `Two rungs have failed the harness. Latest failure:\n${feedback}\n\nDecide whether the approach, the contract or the boundary is wrong before the last attempt.`, 'design before last rung')
|
|
253
|
+
if (!designed && args.effort === 'high')
|
|
254
|
+
return designIncomplete(effort)
|
|
226
255
|
}
|
|
227
256
|
}
|
|
228
257
|
|
|
229
|
-
return { status: 'failed', attempts, lastFailure: feedback }
|
|
258
|
+
return { status: 'failed', attempts, lastFailure: feedback, effort: performedEffort(rungs[rungs.length - 1]) }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
One file per decision that shapes what this project is allowed to claim. A decision lands here when
|
|
4
|
+
reversing it would cost more than making it did, or when a later reader would otherwise re-open it
|
|
5
|
+
from scratch. Read this directory before re-opening a question it already answers, and add a record
|
|
6
|
+
rather than restating a decision in a plan or a commit message.
|
|
7
|
+
|
|
8
|
+
Each record carries four sections: the context it was taken in, the decision itself, the
|
|
9
|
+
consequences it accepts, and how it is enforced.
|
|
10
|
+
|
|
11
|
+
Enforcement is named on a scale, so the strength of a decision is visible rather than implied:
|
|
12
|
+
|
|
13
|
+
| Level | Enforced by |
|
|
14
|
+
|---|---|
|
|
15
|
+
| L0 | text only — nothing checks it |
|
|
16
|
+
| L1 | review — a human is the check |
|
|
17
|
+
| L2 | a local hook — bypassable with `--no-verify` |
|
|
18
|
+
| L3 | CI — it runs on every push |
|
|
19
|
+
| L4 | CI that blocks the merge |
|
|
20
|
+
|
|
21
|
+
A decision enforced only by review says so; that is the honest answer, not a gap to hide.
|
|
22
|
+
|
|
23
|
+
Records are named `NNNN-a-short-title.md` and numbered in the order they were taken.
|
|
24
|
+
|
|
25
|
+
| # | Decision | Enforced at |
|
|
26
|
+
|---|---|---|
|
|
@@ -68,7 +68,9 @@ isolated and manageable — and to treat accidental complexity as a defect.
|
|
|
68
68
|
reconsider the decomposition, coupling or abstraction instead of pushing through.
|
|
69
69
|
|
|
70
70
|
The checklists that turn these principles into questions to answer before a change are in
|
|
71
|
-
[checklists.md](checklists.md).
|
|
71
|
+
[checklists.md](checklists.md). A decision that shapes what this project may claim is recorded once in
|
|
72
|
+
[decisions/](decisions/README.md), with the level at which it is enforced, instead of being restated in
|
|
73
|
+
a plan or a commit message.
|
|
72
74
|
|
|
73
75
|
## Security
|
|
74
76
|
|
/package/templates/presets/monorepo/{baseline → sample}/scripts/tests/lint/syntax-policy.test.ts.eta
RENAMED
|
File without changes
|
/package/templates/presets/node-backend/{baseline → sample}/scripts/tests/lint/syntax-policy.test.ts
RENAMED
|
File without changes
|
|
File without changes
|