@erclx/aitk 0.12.0 → 0.14.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.12.0",
4
+ "version": "0.14.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -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
- - Do not include comments except the shebang line.
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
- - Do not include comments except the shebang line.
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 denials from a run |
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 and the turn ceiling 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.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.14.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -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
- # The index engine that used to live here is TypeScript, in src/indexes/.
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"
@@ -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
@@ -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. Those go in code comments.
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