@erclx/aitk 0.12.0 → 0.13.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/bash-script/SKILL.md +1 -1
- package/claude/skills/claude-review/SKILL.md +2 -0
- package/claude/skills/cli-script/SKILL.md +1 -1
- package/docs/agents.md +6 -2
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/stacks/base.toml +1 -1
- package/package.json +1 -1
- package/scripts/lib/frontmatter.sh +1 -1
- package/src/commands/sandbox.ts +6 -0
- package/src/sandbox/expect.ts +50 -2
- package/standards/architecture.md +1 -1
|
@@ -35,7 +35,7 @@ Load `${CLAUDE_SKILL_DIR}/references/patterns.md` for the timeline lifecycle, lo
|
|
|
35
35
|
- Name functions verb-first: `validate_input`, `deploy_service`, `install_dependencies`.
|
|
36
36
|
- Do not use global variables except exports from `ask()`.
|
|
37
37
|
- Do not define unused color variables.
|
|
38
|
-
-
|
|
38
|
+
- Comment only a fact the reader cannot recover from the code, and follow the code-comment rule in `.claude/rules/` when the project installs it.
|
|
39
39
|
- Use sentence case for section headers and log messages. Proper nouns and product names retain their casing.
|
|
40
40
|
- Quote variables inside parameter expansions: `"${file#"$dir"/}"` not `"${file#$dir/}"`.
|
|
41
41
|
- Quote variables in test brackets: `[ "$i" -eq "$cur" ]` not `[ $i -eq $cur ]`.
|
|
@@ -85,6 +85,8 @@ Flag only issues that will definitely cause incorrect behavior or break a docume
|
|
|
85
85
|
- Linter territory
|
|
86
86
|
- Issues that depend on unverified state
|
|
87
87
|
|
|
88
|
+
A comment that makes a false claim about the code is a correctness finding, not a style one. Flag it past the exclusions above and let the severity ladder rank it.
|
|
89
|
+
|
|
88
90
|
If uncertain, do not flag. False positives erode trust.
|
|
89
91
|
|
|
90
92
|
### Severity
|
|
@@ -33,7 +33,7 @@ Load `${CLAUDE_SKILL_DIR}/references/template.md` for the base skeleton. Copy it
|
|
|
33
33
|
- Decompose by responsibility. Each function does one thing, `main()` orchestrates.
|
|
34
34
|
- Name functions verb-first: `parse_args`, `fetch_data`, `validate_input`.
|
|
35
35
|
- Quote variables in expansions and test brackets.
|
|
36
|
-
-
|
|
36
|
+
- Comment only a fact the reader cannot recover from the code, and follow the code-comment rule in `.claude/rules/` when the project installs it.
|
|
37
37
|
- Use 2-space indentation.
|
|
38
38
|
|
|
39
39
|
## Validation
|
package/docs/agents.md
CHANGED
|
@@ -204,14 +204,16 @@ aitk sandbox check claude:docs drift --json
|
|
|
204
204
|
|
|
205
205
|
| Flag | Effect |
|
|
206
206
|
| ------------------- | ---------------------------------------------------------- |
|
|
207
|
-
| `--envelope <file>` | Read `is_error`, `num_turns`, and
|
|
207
|
+
| `--envelope <file>` | Read `is_error`, `num_turns`, denials, and the reply text |
|
|
208
208
|
| `--writes <file>` | Newline-delimited paths the session wrote, for write scope |
|
|
209
209
|
| `--json` | Emit the verdict record on stdout |
|
|
210
210
|
| `--strict` | Exit 1 on `unchecked` instead of 0 |
|
|
211
211
|
|
|
212
212
|
The verdict `state` is `pass`, `fail`, or `unchecked`. An arm with no `expect.toml` is `unchecked` and exits 0, so the harness stays usable while expectations roll out. A declaration that exists but asserts nothing is a failure, since an expectation file that asserts nothing passes every run.
|
|
213
213
|
|
|
214
|
-
Omitting `--writes` or `--envelope` does not silently drop the assertion kinds that need them. Write scope
|
|
214
|
+
Omitting `--writes` or `--envelope` does not silently drop the assertion kinds that need them. Write scope, the turn ceiling, and the reply assertion report as unchecked and appear in the count, so the standalone command cannot claim more coverage than it had. A verdict never reports `pass` with zero assertions.
|
|
215
|
+
|
|
216
|
+
An envelope that parses but carries no `result` field skips the reply assertion the same way an absent file does. An envelope carrying an empty `result` fails it, since a run that returned no text is a finding rather than a gap in the input.
|
|
215
217
|
|
|
216
218
|
Exit 0 means `pass` or `unchecked`. Exit 1 means `fail`, or a caller error: a malformed target, or a sandbox that was never provisioned. A missing sandbox reports as an error rather than a failed verdict, because failing every path assertion would read as a skill that did nothing. `--strict` moves `unchecked` to exit 1 for a caller that has finished arming its scenarios.
|
|
217
219
|
|
|
@@ -323,6 +325,8 @@ Two exclusions are structural rather than tuning. Heredoc bodies are dropped fro
|
|
|
323
325
|
|
|
324
326
|
The degradation sweep reads its vocabulary from whichever rule publishes a `## Degradation vocabulary` heading, preferring `.claude/rules/` over `governance/rules/`, so one definition serves the toolkit and every target. Discovery anchors on the heading rather than a filename, because a renumbered rule would otherwise empty the vocabulary while the sweep still reported clean. With no such rule the sweep reports **skipped** rather than zero hits, since finding nothing and looking for nothing mean opposite things.
|
|
325
327
|
|
|
328
|
+
`090-code-comments` is the rule that publishes the list, and it ships on the `base` stack. A project that installs or syncs governance for the first time after that rule landed gets a sweep that previously reported skipped, so hits appear where the command used to stay quiet. Edit the backticked terms in the installed copy to change what that project sweeps for. The sweep matches comment text, so a comment naming a term as an example is a hit, and a hit is a prompt to read the line rather than a verdict on it.
|
|
329
|
+
|
|
326
330
|
## Runtime catalogs
|
|
327
331
|
|
|
328
332
|
Use these to discover what's available instead of hardcoding names.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Decide when a code comment should exist and what it may claim
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*.ts'
|
|
5
|
+
- '**/*.tsx'
|
|
6
|
+
- '**/*.js'
|
|
7
|
+
- '**/*.jsx'
|
|
8
|
+
- '**/*.sh'
|
|
9
|
+
- '**/*.py'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Code comment standards
|
|
13
|
+
|
|
14
|
+
## When a comment should exist
|
|
15
|
+
|
|
16
|
+
- Write a comment only when it records a fact the reader cannot recover from the code: an external contract, a rejected alternative, or the reason a surprising line is correct.
|
|
17
|
+
- Do not restate in prose what the line beside it already says.
|
|
18
|
+
- Do not comment a self-contained function whose signature and body already carry its behavior.
|
|
19
|
+
- Let comment density follow how much of a file's behavior is decided outside that file. Treat density as an outcome, never as a target.
|
|
20
|
+
- Do not add or delete a comment to move a file toward a density figure.
|
|
21
|
+
|
|
22
|
+
## What a comment may claim
|
|
23
|
+
|
|
24
|
+
- State only what is true of the code as written.
|
|
25
|
+
- Describe a function's contract and its constraints, never its steps line by line.
|
|
26
|
+
- Update or delete an invalidated comment in the same change that invalidates it.
|
|
27
|
+
- Do not name a person, a ticket, or a date in place of the fact itself.
|
|
28
|
+
|
|
29
|
+
## What never goes in a comment
|
|
30
|
+
|
|
31
|
+
- Delete commented-out code. Do not park it beside the live path.
|
|
32
|
+
- Do not record the edit that produced the code. Version control holds the change history.
|
|
33
|
+
- Do not defer work into a comment. Deferred work belongs in the tracker.
|
|
34
|
+
|
|
35
|
+
## Degradation vocabulary
|
|
36
|
+
|
|
37
|
+
Do not write a comment carrying any of these terms.
|
|
38
|
+
|
|
39
|
+
- `FIXED`, `BUGFIX`, `HACK`, `XXX`, `NOTE:`, `TODO`, `FIXME`, `don't remove`, `previously`, `used to`, `workaround`
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = ""
|
|
2
|
-
rules = ["000-constitution", "010-testing", "020-concurrency", "030-error-handling", "040-performance", "050-logging", "060-naming", "070-planning", "080-config-comments", "500-prose", "510-context", "520-wireframes", "530-requirements", "540-architecture", "550-design", "555-tasks", "560-diagrams", "570-skill", "580-readme", "590-rule-authoring", "591-standard-authoring"]
|
|
2
|
+
rules = ["000-constitution", "010-testing", "020-concurrency", "030-error-handling", "040-performance", "050-logging", "060-naming", "070-planning", "080-config-comments", "090-code-comments", "500-prose", "510-context", "520-wireframes", "530-requirements", "540-architecture", "550-design", "555-tasks", "560-diagrams", "570-skill", "580-readme", "590-rule-authoring", "591-standard-authoring"]
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Kept in bash because the list commands call it once per field per file
|
|
6
6
|
# inside a loop, where shelling into the CLI would cost a process per read.
|
|
7
|
-
#
|
|
7
|
+
# Index logic is TypeScript, in src/indexes/, and does not belong here.
|
|
8
8
|
read_frontmatter_field() {
|
|
9
9
|
local file="$1"
|
|
10
10
|
local key="$2"
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -86,6 +86,11 @@ async function interactivePicker(): Promise<string> {
|
|
|
86
86
|
* file was given, so the turn ceiling reports as skipped rather than passing on a
|
|
87
87
|
* fabricated zero. A file that exists but does not parse falls back to clean,
|
|
88
88
|
* since decision 8 lets the envelope fail a run but never pass one.
|
|
89
|
+
*
|
|
90
|
+
* `result` carries the reply text and is left undefined on both fallbacks, which
|
|
91
|
+
* routes a reply assertion to skipped. Fabricating an empty string there would
|
|
92
|
+
* fail every reply assertion on a run that supplied no envelope, turning a gap in
|
|
93
|
+
* the input into a red arm.
|
|
89
94
|
*/
|
|
90
95
|
function readEnvelope(path: string | undefined): RunEnvelope | undefined {
|
|
91
96
|
if (path === undefined) return undefined
|
|
@@ -102,6 +107,7 @@ function readEnvelope(path: string | undefined): RunEnvelope | undefined {
|
|
|
102
107
|
isError: parsed.is_error === true,
|
|
103
108
|
turns: typeof parsed.num_turns === 'number' ? parsed.num_turns : 0,
|
|
104
109
|
denials: Array.isArray(denials) ? denials.length : 0,
|
|
110
|
+
reply: typeof parsed.result === 'string' ? parsed.result : undefined,
|
|
105
111
|
}
|
|
106
112
|
} catch {
|
|
107
113
|
return CLEAN_ENVELOPE
|
package/src/sandbox/expect.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Expectation {
|
|
|
19
19
|
readonly absent: readonly string[]
|
|
20
20
|
readonly content: readonly ContentAssertion[]
|
|
21
21
|
readonly writeScope: readonly string[]
|
|
22
|
+
readonly reply: readonly string[]
|
|
22
23
|
readonly manual: readonly string[]
|
|
23
24
|
readonly maxTurns?: number
|
|
24
25
|
}
|
|
@@ -28,10 +29,17 @@ export interface AssertionResult {
|
|
|
28
29
|
readonly message: string
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* `reply` is optional because an absent reply and an empty one mean different
|
|
34
|
+
* things. A run whose envelope was never supplied has nothing to assert against
|
|
35
|
+
* and skips. A run that genuinely returned no text carries the empty string and
|
|
36
|
+
* fails every reply assertion, which is the finding.
|
|
37
|
+
*/
|
|
31
38
|
export interface RunEnvelope {
|
|
32
39
|
readonly isError: boolean
|
|
33
40
|
readonly turns: number
|
|
34
41
|
readonly denials: number
|
|
42
|
+
readonly reply?: string
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface Verdict {
|
|
@@ -155,6 +163,7 @@ export function parseExpectation(source: string): Expectation {
|
|
|
155
163
|
absent: stringArray(parsed.absent),
|
|
156
164
|
content: contentArray(parsed.content),
|
|
157
165
|
writeScope: stringArray(parsed.write_scope),
|
|
166
|
+
reply: stringArray(parsed.reply),
|
|
158
167
|
manual: stringArray(parsed.manual),
|
|
159
168
|
maxTurns:
|
|
160
169
|
typeof parsed.max_turns === 'number' ? parsed.max_turns : undefined,
|
|
@@ -171,7 +180,8 @@ export function countMechanicalAssertions(expectation: Expectation): number {
|
|
|
171
180
|
expectation.paths.length +
|
|
172
181
|
expectation.absent.length +
|
|
173
182
|
expectation.content.length +
|
|
174
|
-
expectation.writeScope.length
|
|
183
|
+
expectation.writeScope.length +
|
|
184
|
+
expectation.reply.length
|
|
175
185
|
)
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -259,6 +269,42 @@ function checkWriteScope(
|
|
|
259
269
|
}
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Plain substrings, matched case-sensitively, against the text the run replied
|
|
274
|
+
* with. A substring rather than a regex because the pattern a reply assertion
|
|
275
|
+
* wants is a load-bearing token, a path or a command, and a regex invites the
|
|
276
|
+
* anchored sentence that goes red on any rewording.
|
|
277
|
+
*
|
|
278
|
+
* Declare only positives. A negative substring passes on every reply that
|
|
279
|
+
* phrases the thing differently, which is the vacuous pass
|
|
280
|
+
* `countMechanicalAssertions` excludes `manual` to prevent. An entry asserting
|
|
281
|
+
* what a run must not have said stays in `manual` with its reason.
|
|
282
|
+
*/
|
|
283
|
+
function checkReply(
|
|
284
|
+
expectation: Expectation,
|
|
285
|
+
envelope: RunEnvelope | undefined,
|
|
286
|
+
): KindOutcome {
|
|
287
|
+
if (expectation.reply.length === 0) return { results: [], skipped: [] }
|
|
288
|
+
|
|
289
|
+
if (envelope?.reply === undefined) {
|
|
290
|
+
return {
|
|
291
|
+
results: [],
|
|
292
|
+
skipped: ['reply: no reply text supplied, pass --envelope'],
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const reply = envelope.reply
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
results: expectation.reply.map((fragment) =>
|
|
300
|
+
reply.includes(fragment)
|
|
301
|
+
? { ok: true, message: `reply says: ${fragment}` }
|
|
302
|
+
: { ok: false, message: `reply never says: ${fragment}` },
|
|
303
|
+
),
|
|
304
|
+
skipped: [],
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
262
308
|
/**
|
|
263
309
|
* The envelope never determines a pass. It can only fail a run the expectations
|
|
264
310
|
* would otherwise have passed. Under `bypassPermissions` the denial count is
|
|
@@ -306,16 +352,18 @@ export function checkExpectation(
|
|
|
306
352
|
input: CheckInput,
|
|
307
353
|
): Verdict {
|
|
308
354
|
const scope = checkWriteScope(expectation, input.writes)
|
|
355
|
+
const reply = checkReply(expectation, input.envelope)
|
|
309
356
|
const envelope = checkEnvelope(expectation, input.envelope)
|
|
310
357
|
|
|
311
358
|
const results = [
|
|
312
359
|
...checkPaths(expectation, input.sandboxDir),
|
|
313
360
|
...checkAbsent(expectation, input.sandboxDir),
|
|
314
361
|
...checkContent(expectation, input.sandboxDir),
|
|
362
|
+
...reply.results,
|
|
315
363
|
...scope.results,
|
|
316
364
|
...envelope.results,
|
|
317
365
|
]
|
|
318
|
-
const skipped = [...scope.skipped, ...envelope.skipped]
|
|
366
|
+
const skipped = [...scope.skipped, ...reply.skipped, ...envelope.skipped]
|
|
319
367
|
|
|
320
368
|
const failed = results.filter((result) => !result.ok).length
|
|
321
369
|
|
|
@@ -17,7 +17,7 @@ Applies to `.claude/ARCHITECTURE.md`. Describes the system shape and the decisio
|
|
|
17
17
|
|
|
18
18
|
- Per-domain structure and narrative. That belongs in `.claude/context/<domain>.md`, one file per domain.
|
|
19
19
|
- Setup commands and install instructions. Those live in the README.
|
|
20
|
-
- How individual functions work line by line.
|
|
20
|
+
- How individual functions work line by line. The code carries its own behavior.
|
|
21
21
|
- Full type definitions. They live in code. Reference the shape conceptually if needed.
|
|
22
22
|
|
|
23
23
|
## Sections
|