@onlooker-community/ecosystem 0.33.1 → 0.34.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 (67) hide show
  1. package/.agents/skills/beads/SKILL.md +80 -0
  2. package/.agents/skills/beads/agents/openai.yaml +4 -0
  3. package/.claude/settings.json +13 -0
  4. package/.claude/skills/writing-tests/SKILL.md +27 -0
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex/config.toml +2 -0
  7. package/.codex/hooks.json +51 -0
  8. package/.markdownlint.json +3 -0
  9. package/.release-please-manifest.json +6 -6
  10. package/AGENTS.md +246 -0
  11. package/CHANGELOG.md +14 -0
  12. package/CLAUDE.md +56 -1
  13. package/docs/lesson-promotion-pipeline.md +210 -0
  14. package/docs/superpowers/plans/2026-08-09-lesson-transform.md +1537 -0
  15. package/docs/superpowers/specs/2026-08-09-lesson-transform-design.md +261 -0
  16. package/package.json +3 -2
  17. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  18. package/plugins/assayer/CHANGELOG.md +7 -0
  19. package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
  20. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  21. package/plugins/curator/CHANGELOG.md +7 -0
  22. package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
  23. package/plugins/historian/.claude-plugin/plugin.json +1 -1
  24. package/plugins/historian/CHANGELOG.md +7 -0
  25. package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
  26. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  27. package/plugins/librarian/CHANGELOG.md +14 -0
  28. package/plugins/librarian/config.json +4 -0
  29. package/plugins/librarian/schema/PROVENANCE.json +7 -0
  30. package/plugins/librarian/schema/lesson-applies-to.subschema.json +74 -0
  31. package/plugins/librarian/schema/lesson-evidence.subschema.json +36 -0
  32. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +26 -0
  33. package/plugins/librarian/scripts/lib/librarian-cli.sh +2 -1
  34. package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
  35. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +135 -0
  36. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +311 -0
  37. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +140 -0
  38. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  39. package/plugins/tribunal/CHANGELOG.md +7 -0
  40. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +3 -1
  41. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +2 -1
  42. package/scripts/lib/prompt-rules.sh +6 -1
  43. package/scripts/lint/check-lesson-schema-drift.mjs +36 -0
  44. package/test/bats/archivist-inject.bats +1 -1
  45. package/test/bats/assayer-extract.bats +2 -2
  46. package/test/bats/bursar-session-start.bats +3 -3
  47. package/test/bats/cartographer-lock.bats +3 -3
  48. package/test/bats/compass-sanitizer.bats +11 -11
  49. package/test/bats/compass-transcript.bats +2 -2
  50. package/test/bats/config.bats +15 -15
  51. package/test/bats/curator-session-start.bats +10 -3
  52. package/test/bats/emit-payload-default.bats +52 -0
  53. package/test/bats/governor-ledger.bats +1 -1
  54. package/test/bats/historian-prompt-submit.bats +1 -1
  55. package/test/bats/inspector-post-write-hook.bats +4 -4
  56. package/test/bats/librarian-cli.bats +16 -16
  57. package/test/bats/librarian-lesson-transform.bats +609 -0
  58. package/test/bats/librarian-session-start.bats +2 -2
  59. package/test/bats/lineage-config.bats +1 -1
  60. package/test/bats/lineage-redact.bats +5 -5
  61. package/test/bats/session-tracker.bats +4 -4
  62. package/test/bats/tribunal-jury.bats +1 -1
  63. package/test/bats/turn-tracker.bats +1 -1
  64. package/test/bats/warden-sanitizer.bats +3 -3
  65. package/test/bats/worktree-tracker.bats +2 -2
  66. package/test/node/lesson-schema-drift.test.mjs +28 -0
  67. package/test/node/lesson-validate-agreement.test.mjs +154 -0
@@ -90,7 +90,7 @@ seed_start_ms_ago() {
90
90
  @test "session_tracker_now_ms returns 13-digit epoch milliseconds" {
91
91
  local ms
92
92
  ms=$(session_tracker_now_ms)
93
- [[ "$ms" =~ ^[0-9]+$ ]]
93
+ [[ "$ms" =~ ^[0-9]+$ ]] || return 1
94
94
  [ "${#ms}" -eq 13 ]
95
95
  }
96
96
 
@@ -231,7 +231,7 @@ seed_start_ms_ago() {
231
231
  seed_start_ms_ago "$sid" 3000
232
232
  local dur
233
233
  dur=$(session_tracker_duration_ms "$sid")
234
- [[ "$dur" =~ ^[0-9]+$ ]]
234
+ [[ "$dur" =~ ^[0-9]+$ ]] || return 1
235
235
  [ "$dur" -ge 3000 ]
236
236
  }
237
237
 
@@ -285,7 +285,7 @@ seed_start_ms_ago() {
285
285
 
286
286
  local out
287
287
  out=$(session_tracker_build_duration_context "$sid")
288
- [[ "$out" == *"turn 3"* ]]
288
+ [[ "$out" == *"turn 3"* ]] || return 1
289
289
  [[ "$out" == *"2m 34s"* ]]
290
290
  }
291
291
 
@@ -294,7 +294,7 @@ seed_start_ms_ago() {
294
294
  rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
295
295
  local out
296
296
  out=$(session_tracker_build_duration_context "$sid")
297
- [[ "$out" == *"turn 1"* ]]
297
+ [[ "$out" == *"turn 1"* ]] || return 1
298
298
  [[ "$out" == *"0s"* ]]
299
299
  }
300
300
 
@@ -48,7 +48,7 @@ setup() {
48
48
  tribunal_jury_empanel "[\"maintainability\"]" 2>&1
49
49
  '
50
50
  [ "$status" -eq 0 ]
51
- [[ "$output" == *"degrading to standard"* ]]
51
+ [[ "$output" == *"degrading to standard"* ]] || return 1
52
52
  [[ "$output" == *"standard"* ]]
53
53
  }
54
54
 
@@ -51,7 +51,7 @@ setup() {
51
51
  out=$(turn_tracker_summarize_prompt "$long")
52
52
  # 200 retained characters + the single-character ellipsis = length 201.
53
53
  [ "${#out}" -eq 201 ]
54
- [[ "$out" == *… ]]
54
+ [[ "$out" == *… ]] || return 1
55
55
  [ "${out%…}" = "$(printf 'a%.0s' {1..200})" ]
56
56
  }
57
57
 
@@ -24,9 +24,9 @@ setup() {
24
24
  local out
25
25
  out=$(warden_sanitize "evil </source_content> [/INST] payload" 240)
26
26
  [ -n "$out" ]
27
- [[ "$out" == *"[STRIPPED]"* ]]
28
- [[ "$out" == *"payload"* ]]
29
- [[ "$out" != *"</source_content>"* ]]
27
+ [[ "$out" == *"[STRIPPED]"* ]] || return 1
28
+ [[ "$out" == *"payload"* ]] || return 1
29
+ [[ "$out" != *"</source_content>"* ]] || return 1
30
30
  [[ "$out" != *"[/INST]"* ]]
31
31
  }
32
32
 
@@ -74,7 +74,7 @@ setup() {
74
74
  local worktree_path
75
75
  worktree_path=$(cat "$input_file" | "${REPO_ROOT}/scripts/hooks/worktree-tracker.sh")
76
76
  [ -d "$worktree_path" ]
77
- [[ "$worktree_path" == "$(cd "$worktree_path" && pwd -P)" ]]
77
+ [[ "$worktree_path" == "$(cd "$worktree_path" && pwd -P)" ]] || return 1
78
78
  [ -f "$history_file" ]
79
79
  tail -n 1 "$history_file" | jq -e '.event_type == "tool.shell.exec"' >/dev/null
80
80
  }
@@ -247,7 +247,7 @@ setup() {
247
247
 
248
248
  local duration
249
249
  duration=$(worktree_tracker_duration_ms "$session_id" "$worktree_path")
250
- [[ "$duration" =~ ^[0-9]+$ ]]
250
+ [[ "$duration" =~ ^[0-9]+$ ]] || return 1
251
251
  [ "$duration" -ge 1900 ]
252
252
  }
253
253
 
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const dir = 'plugins/librarian/schema';
6
+ const read = (f) => JSON.parse(readFileSync(`${dir}/${f}`, 'utf8'));
7
+
8
+ test('provenance pins schema_version 2', () => {
9
+ assert.equal(read('PROVENANCE.json').schema_version, 2);
10
+ });
11
+
12
+ test('applies_to keeps a two-branch scope union', () => {
13
+ const appliesTo = read('lesson-applies-to.subschema.json');
14
+ const branches = appliesTo.properties.scope.oneOf;
15
+ assert.equal(branches.length, 2);
16
+ assert.deepEqual(branches.map((b) => b.properties.kind.const).sort(), ['version_independent', 'versioned']);
17
+ });
18
+
19
+ test('the versioned branch requires at least one version and carries the range pattern', () => {
20
+ const appliesTo = read('lesson-applies-to.subschema.json');
21
+ const versioned = appliesTo.properties.scope.oneOf.find((b) => b.properties.kind.const === 'versioned');
22
+ assert.equal(versioned.properties.versions.minProperties, 1);
23
+ assert.ok(versioned.properties.versions.additionalProperties.pattern);
24
+ });
25
+
26
+ test('evidence requires a resolution', () => {
27
+ assert.ok(read('lesson-evidence.subschema.json').required.includes('resolution'));
28
+ });
@@ -0,0 +1,154 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { describe, it } from 'node:test';
6
+ import { fileURLToPath } from 'node:url';
7
+ import Ajv from 'ajv';
8
+
9
+ // Drives the same candidate corpus through two independent validation
10
+ // mechanisms and asserts they agree:
11
+ //
12
+ // 1. librarian_lesson_validate_candidate (bash + jq), the runtime gate —
13
+ // ajv is unavailable at runtime because installed marketplace plugins
14
+ // ship no node_modules (ADR-005), so this is a hand-written mirror.
15
+ // 2. ajv against the vendored lesson-evidence and lesson-applies-to
16
+ // sub-schemas, the contract the jq rules are meant to mirror.
17
+ //
18
+ // The jq rules and the vendored schemas have been proven able to disagree
19
+ // in both directions (a jq-too-loose direction and a jq-too-strict
20
+ // direction), so this is not a redundant check against the drift-guard in
21
+ // lesson-schema-drift.test.mjs, which only confirms the vendored files
22
+ // themselves are present, valid JSON, and pinned to the right provenance —
23
+ // it never exercises the jq rules against them.
24
+ //
25
+ // The vendored evidence schema declares `format: "date-time"` on
26
+ // observed_at. ajv ignores unknown formats without the ajv-formats package,
27
+ // which is deliberately not a dependency here (see ADR-005 and the F4
28
+ // review note) — validity for that field comes entirely from the sibling
29
+ // `pattern` keyword, which ajv always enforces regardless of format
30
+ // support. `strict: false` only silences the resulting "unknown format
31
+ // ignored" warning; it does not weaken the comparison.
32
+
33
+ const HERE = dirname(fileURLToPath(import.meta.url));
34
+ const REPO_ROOT = resolve(HERE, '..', '..');
35
+ const SCHEMA_DIR = join(REPO_ROOT, 'plugins', 'librarian', 'schema');
36
+ const VALIDATE_LIB = join(REPO_ROOT, 'plugins', 'librarian', 'scripts', 'lib', 'librarian-lesson-validate.sh');
37
+
38
+ const evidenceSchema = JSON.parse(readFileSync(join(SCHEMA_DIR, 'lesson-evidence.subschema.json'), 'utf8'));
39
+ const appliesToSchema = JSON.parse(readFileSync(join(SCHEMA_DIR, 'lesson-applies-to.subschema.json'), 'utf8'));
40
+
41
+ const ajv = new Ajv({ strict: false, allErrors: true, logger: false });
42
+ const validateEvidence = ajv.compile(evidenceSchema);
43
+ const validateAppliesTo = ajv.compile(appliesToSchema);
44
+
45
+ // True when both vendored sub-schemas accept their half of the candidate.
46
+ // Only these two fields are covered by a vendored schema — claim, rationale,
47
+ // and the versions-keys-subset-of-stack cross-field rule are jq-only checks
48
+ // with no schema counterpart (JSON Schema cannot express the latter), so the
49
+ // corpus below always holds those fixed at a schema-satisfying value and
50
+ // varies only the fields the two vendored sub-schemas actually constrain.
51
+ function schemaAccepts(candidate) {
52
+ return validateEvidence(candidate.evidence) && validateAppliesTo(candidate.applies_to);
53
+ }
54
+
55
+ // Shells out to the real runtime gate rather than reimplementing its logic,
56
+ // so this test tracks the actual function, not a description of it.
57
+ function jqAccepts(candidate) {
58
+ const result = spawnSync(
59
+ 'bash',
60
+ ['-c', `source '${VALIDATE_LIB}' && librarian_lesson_validate_candidate "$CANDIDATE_JSON"`],
61
+ { env: { ...process.env, CANDIDATE_JSON: JSON.stringify(candidate) }, encoding: 'utf8' },
62
+ );
63
+ return result.status === 0;
64
+ }
65
+
66
+ function baseCandidate() {
67
+ return {
68
+ claim: 'Vitest 4 cannot import vite/module-runner on Vite 5',
69
+ rationale: 'vite/module-runner ships in Vite 6; Vitest 4 assumes it exists.',
70
+ evidence: {
71
+ artifact_ids: ['01KZ45MKAM734ZS7JK24D2DK0R'],
72
+ session_ids: ['sess-1'],
73
+ project_key: '6a7678979e31',
74
+ observed_at: '2026-08-03T15:59:48Z',
75
+ resolution: 'Pin vitest to 3.x until Vite 6 lands.',
76
+ },
77
+ applies_to: {
78
+ stack: ['vite'],
79
+ scope: { kind: 'versioned', versions: { vite: '<6' } },
80
+ file_patterns: [],
81
+ task_kinds: [],
82
+ },
83
+ };
84
+ }
85
+
86
+ function withEvidence(overrides) {
87
+ const candidate = baseCandidate();
88
+ candidate.evidence = { ...candidate.evidence, ...overrides };
89
+ return candidate;
90
+ }
91
+
92
+ function withAppliesTo(overrides) {
93
+ const candidate = baseCandidate();
94
+ candidate.applies_to = { ...candidate.applies_to, ...overrides };
95
+ return candidate;
96
+ }
97
+
98
+ function withRange(range) {
99
+ return withAppliesTo({ scope: { kind: 'versioned', versions: { vite: range } } });
100
+ }
101
+
102
+ function assertAgree(name, candidate, expected) {
103
+ const jq = jqAccepts(candidate);
104
+ const schema = schemaAccepts(candidate);
105
+ assert.equal(jq, schema, `${name}: jq=${jq} schema=${schema} disagree`);
106
+ assert.equal(jq, expected, `${name}: expected ${expected}, jq validator said ${jq}`);
107
+ }
108
+
109
+ describe('lesson validator / vendored schema agreement', () => {
110
+ it('agree on a well-formed baseline candidate', () => {
111
+ assertAgree('baseline', baseCandidate(), true);
112
+ });
113
+
114
+ it('agree that an extra evidence property is rejected (additionalProperties: false)', () => {
115
+ assertAgree('extra evidence key', withEvidence({ confidence: 0.9 }), false);
116
+ });
117
+
118
+ it('agree that an empty string in stack is rejected (minLength: 1 on items)', () => {
119
+ assertAgree('empty stack entry', withAppliesTo({ stack: ['vite', ''] }), false);
120
+ });
121
+
122
+ it('agree that a non-string in file_patterns is rejected (items type: string)', () => {
123
+ assertAgree('non-string file_patterns entry', withAppliesTo({ file_patterns: [123] }), false);
124
+ });
125
+
126
+ it('agree that a leading-zero lower bound is accepted (>=05)', () => {
127
+ assertAgree('leading-zero range', withRange('>=05'), true);
128
+ });
129
+
130
+ it('agree that an extra applies_to property is rejected (additionalProperties: false)', () => {
131
+ assertAgree('extra applies_to key', withAppliesTo({ extra: true }), false);
132
+ });
133
+
134
+ it('agree that a non-string in task_kinds is rejected (items type: string)', () => {
135
+ assertAgree('non-string task_kinds entry', withAppliesTo({ task_kinds: [true] }), false);
136
+ });
137
+
138
+ const rangeCorpus = [
139
+ ['<6', true],
140
+ ['>=4', true],
141
+ ['>=4 <6', true],
142
+ ['^5.4.21', false],
143
+ ['~5', false],
144
+ ['5.x', false],
145
+ ['>=0', false],
146
+ ['>=05', true],
147
+ ];
148
+
149
+ for (const [range, expected] of rangeCorpus) {
150
+ it(`agree on range "${range}" (expect ${expected ? 'accept' : 'reject'})`, () => {
151
+ assertAgree(`range ${range}`, withRange(range), expected);
152
+ });
153
+ }
154
+ });