@jjchill/probity-rules 0.1.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/GLOSSARY.template.md +33 -0
  3. package/README.md +96 -0
  4. package/dist/index.d.ts +20 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +20 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/presets/js.d.ts +63 -0
  9. package/dist/presets/js.d.ts.map +1 -0
  10. package/dist/presets/js.js +112 -0
  11. package/dist/presets/js.js.map +1 -0
  12. package/dist/presets/kmp.d.ts +27 -0
  13. package/dist/presets/kmp.d.ts.map +1 -0
  14. package/dist/presets/kmp.js +253 -0
  15. package/dist/presets/kmp.js.map +1 -0
  16. package/dist/presets/kotlin.d.ts +40 -0
  17. package/dist/presets/kotlin.d.ts.map +1 -0
  18. package/dist/presets/kotlin.js +172 -0
  19. package/dist/presets/kotlin.js.map +1 -0
  20. package/dist/presets/swift.d.ts +10 -0
  21. package/dist/presets/swift.d.ts.map +1 -0
  22. package/dist/presets/swift.js +285 -0
  23. package/dist/presets/swift.js.map +1 -0
  24. package/dist/rules/acceptance-language.d.ts +95 -0
  25. package/dist/rules/acceptance-language.d.ts.map +1 -0
  26. package/dist/rules/acceptance-language.js +443 -0
  27. package/dist/rules/acceptance-language.js.map +1 -0
  28. package/dist/rules/gates.d.ts +125 -0
  29. package/dist/rules/gates.d.ts.map +1 -0
  30. package/dist/rules/gates.js +285 -0
  31. package/dist/rules/gates.js.map +1 -0
  32. package/dist/rules/kotlin.d.ts +323 -0
  33. package/dist/rules/kotlin.d.ts.map +1 -0
  34. package/dist/rules/kotlin.js +722 -0
  35. package/dist/rules/kotlin.js.map +1 -0
  36. package/dist/rules/ports-and-adapters.d.ts +86 -0
  37. package/dist/rules/ports-and-adapters.d.ts.map +1 -0
  38. package/dist/rules/ports-and-adapters.js +366 -0
  39. package/dist/rules/ports-and-adapters.js.map +1 -0
  40. package/dist/rules/scoping.d.ts +68 -0
  41. package/dist/rules/scoping.d.ts.map +1 -0
  42. package/dist/rules/scoping.js +93 -0
  43. package/dist/rules/scoping.js.map +1 -0
  44. package/dist/rules/spec-test-parity.d.ts +164 -0
  45. package/dist/rules/spec-test-parity.d.ts.map +1 -0
  46. package/dist/rules/spec-test-parity.js +456 -0
  47. package/dist/rules/spec-test-parity.js.map +1 -0
  48. package/dist/rules/swift.d.ts +50 -0
  49. package/dist/rules/swift.d.ts.map +1 -0
  50. package/dist/rules/swift.js +50 -0
  51. package/dist/rules/swift.js.map +1 -0
  52. package/dist/rules/ubiquitous-language.d.ts +36 -0
  53. package/dist/rules/ubiquitous-language.d.ts.map +1 -0
  54. package/dist/rules/ubiquitous-language.js +140 -0
  55. package/dist/rules/ubiquitous-language.js.map +1 -0
  56. package/dist/scripts/scope-report.d.ts +3 -0
  57. package/dist/scripts/scope-report.d.ts.map +1 -0
  58. package/dist/scripts/scope-report.js +184 -0
  59. package/dist/scripts/scope-report.js.map +1 -0
  60. package/kiro/README.md +20 -0
  61. package/kiro/kiro-agent.template.json +45 -0
  62. package/kiro/kiro-transcript-to-claude.py +183 -0
  63. package/kiro/probity-kiro-translate.py +132 -0
  64. package/kiro/probity-kiro.sh +87 -0
  65. package/kiro/skill-activation-forced-eval.sh +45 -0
  66. package/package.json +68 -0
  67. package/probity.config.kmp.ts +40 -0
  68. package/probity.config.kotlin.ts +37 -0
  69. package/probity.config.swift.ts +38 -0
  70. package/probity.config.ts +43 -0
  71. package/scripts/spec-parity.mjs +345 -0
@@ -0,0 +1,140 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ /**
4
+ * Glossary drift protection for the `ubiquitous-language` skill.
5
+ *
6
+ * Glossary format convention (see GLOSSARY.template.md): a Markdown
7
+ * file where each term is a level-2 heading:
8
+ *
9
+ * ## Parcel
10
+ * A shipment registered for delivery to a recipient.
11
+ *
12
+ * One term per concept; the definition text is free-form. Terms are
13
+ * matched case-insensitively.
14
+ */
15
+ const TERM_HEADING = /^##\s+(.+?)\s*$/gm;
16
+ export function extractGlossaryTerms(content) {
17
+ return [...content.matchAll(TERM_HEADING)].map((m) => (m[1] ?? '').trim());
18
+ }
19
+ function escapeRegex(text) {
20
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
+ }
22
+ /**
23
+ * A term is "used" when it appears as a phrase in prose (case-
24
+ * insensitive, whitespace-tolerant) or as an identifier in code —
25
+ * PascalCase, camelCase, or snake_case concatenations of its words
26
+ * ("Delivery Window" → DeliveryWindow, deliveryWindow,
27
+ * delivery_window).
28
+ */
29
+ export function termUsagePattern(term) {
30
+ const words = term.trim().split(/\s+/).map(escapeRegex);
31
+ const phrase = words.join('\\s+');
32
+ if (words.length === 1)
33
+ return new RegExp(`\\b${phrase}\\b`, 'i');
34
+ const capitalized = term
35
+ .trim()
36
+ .split(/\s+/)
37
+ .map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase());
38
+ const pascal = escapeRegex(capitalized.join(''));
39
+ const camel = escapeRegex(capitalized[0].toLowerCase() + capitalized.slice(1).join(''));
40
+ const snake = words.map((w) => w.toLowerCase()).join('_');
41
+ return new RegExp(`\\b(?:${phrase}|${pascal}|${camel}|${snake})\\b`, 'i');
42
+ }
43
+ const SKIP_DIRS = new Set([
44
+ 'node_modules',
45
+ '.git',
46
+ 'build',
47
+ '.gradle',
48
+ 'out',
49
+ 'dist',
50
+ '.idea',
51
+ ]);
52
+ function walk(dir, out = []) {
53
+ let entries;
54
+ try {
55
+ entries = readdirSync(dir, { withFileTypes: true });
56
+ }
57
+ catch {
58
+ return out;
59
+ }
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ if (!SKIP_DIRS.has(entry.name))
63
+ walk(join(dir, entry.name), out);
64
+ }
65
+ else {
66
+ out.push(join(dir, entry.name));
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ const DEFAULT_SEARCH_PATTERN = /\.(kt|kts|swift|ts|js|py|java|feature|md)$/;
72
+ /**
73
+ * Surfaces glossary drift at the moment it is created: when a write
74
+ * to the glossary removes or renames a term that specs, tests, or
75
+ * source code still use, the write is blocked with the list of files
76
+ * using the term — rename the usages in the same change (or record
77
+ * the old term's fate in the glossary entry) and re-apply. Removing
78
+ * a term nothing uses passes silently, as do additions and
79
+ * definition edits.
80
+ *
81
+ * The companion invariant — new spec/port vocabulary conforming to
82
+ * the glossary — is judged by `enforceAcceptanceLanguage` and
83
+ * `enforcePortsBoundary` when given `glossaryPath`.
84
+ *
85
+ * Applies to: write actions to the glossary file (scope via a
86
+ * `{ files, rules }` block on its path). Deterministic — no AI call.
87
+ *
88
+ * @param options.searchRoots — absolute paths to scan for usages
89
+ * (repo root is fine; node_modules/build dirs are skipped).
90
+ * @param options.searchPattern — which files count as usage sites
91
+ * (default: common source, spec, and doc extensions).
92
+ */
93
+ export function surfaceGlossaryTermBreakage(options) {
94
+ const pattern = options.searchPattern ?? DEFAULT_SEARCH_PATTERN;
95
+ return async function surfaceGlossaryTermBreakage(action, ctx) {
96
+ if (action.kind !== 'write')
97
+ return { kind: 'pass' };
98
+ const before = await ctx?.readFile?.(action.path);
99
+ if (!before || before.kind !== 'present')
100
+ return { kind: 'pass' };
101
+ const beforeTerms = extractGlossaryTerms(before.content);
102
+ const afterTerms = new Set(extractGlossaryTerms(action.content).map((t) => t.toLowerCase()));
103
+ const removed = beforeTerms.filter((t) => !afterTerms.has(t.toLowerCase()));
104
+ if (removed.length === 0)
105
+ return { kind: 'pass' };
106
+ const glossaryPath = resolve(action.path);
107
+ const files = options.searchRoots
108
+ .flatMap((root) => walk(root))
109
+ .filter((file) => pattern.test(file) && resolve(file) !== glossaryPath);
110
+ const broken = [];
111
+ for (const term of removed) {
112
+ const usage = termUsagePattern(term);
113
+ const users = files.filter((file) => {
114
+ try {
115
+ return usage.test(readFileSync(file, 'utf8'));
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ });
121
+ if (users.length > 0) {
122
+ const shown = users.slice(0, 10).map((f) => ` ${f}`);
123
+ if (users.length > 10)
124
+ shown.push(` …and ${users.length - 10} more`);
125
+ broken.push(`"${term}" is still used by:\n${shown.join('\n')}`);
126
+ }
127
+ }
128
+ if (broken.length === 0)
129
+ return { kind: 'pass' };
130
+ return {
131
+ kind: 'violation',
132
+ reason: 'This edit removes or renames glossary term(s) that specs, ' +
133
+ 'tests, or code still use. The glossary is the source of ' +
134
+ 'truth: update the usages in the same change, then re-apply ' +
135
+ 'this edit.\n' +
136
+ broken.join('\n'),
137
+ };
138
+ };
139
+ }
140
+ //# sourceMappingURL=ubiquitous-language.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ubiquitous-language.js","sourceRoot":"","sources":["../../rules/ubiquitous-language.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=scope-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-report.d.ts","sourceRoot":"","sources":["../../scripts/scope-report.ts"],"names":[],"mappings":""}
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scoping dry-run for a Probity config: shows which files each
4
+ * `{ files, rules }` block actually claims in YOUR repo, before the
5
+ * first agent session finds out the hard way.
6
+ *
7
+ * Wrong glob scoping is the template set's main failure mode, and it
8
+ * fails in two directions: over-scoping is loud (enforcePortsBoundary
9
+ * pointed at adapter files blocks work those files are supposed to
10
+ * do), but under-scoping is silent — a glob slightly too narrow for
11
+ * your layout means enforcement simply never fires, and nothing
12
+ * notices. This report surfaces both at setup time:
13
+ *
14
+ * - DEAD SCOPE: a block whose globs match zero files
15
+ * - core-purity rules claiming adapter/DI/UI-looking paths
16
+ * - the acceptance-language rule claiming Robot/driver/DSL files
17
+ *
18
+ * Globs are resolved exactly as Probity resolves them — anchoring and
19
+ * picomatch semantics come from rules/scoping.ts, the same replica
20
+ * the workflow eval uses.
21
+ *
22
+ * Usage (from your project root, next to your probity.config.ts):
23
+ *
24
+ * npx probity-scope-report [--config probity.config.ts]
25
+ * [--root .] [--strict]
26
+ *
27
+ * --config defaults to probity.config.{ts,mts,js,mjs} found in
28
+ * --root; --root defaults to the current working directory. --strict
29
+ * exits 1 when any warning fires (for CI); otherwise warnings are
30
+ * advisory and the exit code is 0.
31
+ *
32
+ * The config is loaded via jiti — the same TypeScript loader Probity
33
+ * itself uses — rather than a bare dynamic `import()` of a .ts file,
34
+ * so this works whether the consumer's config is .ts, .mts, .js, or
35
+ * .mjs, with no separate build step for the config itself.
36
+ */
37
+ import { existsSync, readdirSync } from 'node:fs';
38
+ import { dirname, join, relative, resolve } from 'node:path';
39
+ import { createJiti } from 'jiti';
40
+ import { anchorGlob, buildMatcher, isRuleBlock } from '../rules/scoping.js';
41
+ const jiti = createJiti(import.meta.url);
42
+ const SKIP_DIRS = new Set([
43
+ 'node_modules',
44
+ '.git',
45
+ 'build',
46
+ '.gradle',
47
+ 'out',
48
+ 'dist',
49
+ '.idea',
50
+ ]);
51
+ const SAMPLE_LIMIT = 6;
52
+ // Paths a core-purity rule should normally NOT claim (adapters, DI,
53
+ // UI, platform source sets) and a spec-language rule should normally
54
+ // NOT claim (layer-2 drivers). Heuristics: a hit is a warning to
55
+ // review, not proof of misconfiguration.
56
+ const CORE_RULES = /PortsBoundary|forbidNewAmbientEffects|forbidContentPattern/;
57
+ const ADAPTERISH_SEGMENT = /^(adapter|adapters|infra|infrastructure|ui|di)$/;
58
+ const LANGUAGE_RULES = /AcceptanceLanguage/;
59
+ const DRIVERISH_FILE = /(Robot|Driver|Dsl)\.\w+$|[/\\](drivers?|dsl)[/\\]/;
60
+ function parseArgs(argv) {
61
+ const out = { strict: false };
62
+ for (let i = 0; i < argv.length; i++) {
63
+ if (argv[i] === '--config')
64
+ out.config = argv[++i];
65
+ else if (argv[i] === '--root')
66
+ out.root = argv[++i];
67
+ else if (argv[i] === '--strict')
68
+ out.strict = true;
69
+ else {
70
+ console.error(`Unknown argument: ${argv[i]}`);
71
+ process.exit(2);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ function findConfig(dir) {
77
+ for (const ext of ['ts', 'mts', 'js', 'mjs']) {
78
+ const candidate = join(dir, `probity.config.${ext}`);
79
+ if (existsSync(candidate))
80
+ return candidate;
81
+ }
82
+ console.error(`No probity.config.{ts,mts,js,mjs} in ${dir} — pass --config.`);
83
+ process.exit(2);
84
+ }
85
+ function walk(dir, out = []) {
86
+ let entries;
87
+ try {
88
+ entries = readdirSync(dir, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return out;
92
+ }
93
+ for (const entry of entries) {
94
+ if (entry.isDirectory()) {
95
+ if (!SKIP_DIRS.has(entry.name))
96
+ walk(join(dir, entry.name), out);
97
+ }
98
+ else {
99
+ out.push(join(dir, entry.name).replace(/\\/g, '/'));
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ function ruleNames(block) {
105
+ return block.rules.map((rule) => rule.name || '(unnamed)').join(', ');
106
+ }
107
+ const args = parseArgs(process.argv.slice(2));
108
+ const root = resolve(args.root ?? process.cwd());
109
+ const configPath = resolve(args.config ?? findConfig(root));
110
+ const configDir = dirname(configPath);
111
+ const config = await jiti.import(configPath, { default: true });
112
+ const tree = walk(root);
113
+ console.log(`Scope report for ${configPath}`);
114
+ console.log(`Repo root: ${root} (${tree.length} files scanned)\n`);
115
+ const warnings = [];
116
+ const claimed = new Set();
117
+ let flatRules = 0;
118
+ for (const [index, entry] of config.rules.entries()) {
119
+ if (!isRuleBlock(entry)) {
120
+ flatRules++;
121
+ continue;
122
+ }
123
+ const label = `block ${index + 1} [${ruleNames(entry)}]`;
124
+ if (!entry.files) {
125
+ console.log(`${label}\n files: (none) — applies to every write\n`);
126
+ continue;
127
+ }
128
+ // Anchor against the config's directory, exactly as loadConfig does.
129
+ const globs = entry.files.map((glob) => anchorGlob(glob, configDir));
130
+ const matched = tree.filter(buildMatcher(globs));
131
+ matched.forEach((file) => claimed.add(file));
132
+ console.log(label);
133
+ // Per-glob counts are informational: a template can legitimately
134
+ // carry globs for layouts a given repo doesn't use (e.g. classic
135
+ // src/main alongside KMP source sets) — only a whole-block zero is
136
+ // a warning.
137
+ for (const glob of entry.files) {
138
+ const count = glob.startsWith('!')
139
+ ? null
140
+ : tree.filter(buildMatcher([anchorGlob(glob, configDir)])).length;
141
+ console.log(` ${glob}${count === null ? '' : ` (${count})`}`);
142
+ }
143
+ console.log(` → ${matched.length} file(s)`);
144
+ for (const file of matched.slice(0, SAMPLE_LIMIT))
145
+ console.log(` ${relative(root, file)}`);
146
+ if (matched.length > SAMPLE_LIMIT)
147
+ console.log(` …and ${matched.length - SAMPLE_LIMIT} more`);
148
+ console.log('');
149
+ if (matched.length === 0) {
150
+ warnings.push(`DEAD SCOPE: ${label} matches no files — these rules will never fire. ` +
151
+ 'Adjust the globs to your layout (this is the silent failure mode).');
152
+ }
153
+ if (CORE_RULES.test(ruleNames(entry))) {
154
+ const suspicious = matched.filter((file) => relative(root, file).split(/[/\\]/).some((segment) => ADAPTERISH_SEGMENT.test(segment)));
155
+ if (suspicious.length > 0) {
156
+ warnings.push(`${label} claims ${suspicious.length} adapter/DI/UI-looking file(s), e.g. ` +
157
+ `${relative(root, suspicious[0])} — core-purity rules pointed at adapters ` +
158
+ 'block work those files are supposed to do. Exclude them (negation globs) ' +
159
+ 'or confirm these paths really are core.');
160
+ }
161
+ }
162
+ if (LANGUAGE_RULES.test(ruleNames(entry))) {
163
+ const suspicious = matched.filter((file) => DRIVERISH_FILE.test(file));
164
+ if (suspicious.length > 0) {
165
+ warnings.push(`${label} claims ${suspicious.length} driver/DSL-looking file(s), e.g. ` +
166
+ `${relative(root, suspicious[0])} — layer-2 drivers are supposed to contain ` +
167
+ 'the mechanics the Language Test blocks. Exclude them (e.g. !**/*Robot.kt).');
168
+ }
169
+ }
170
+ }
171
+ if (flatRules > 0) {
172
+ console.log(`${flatRules} flat rule(s) apply to every action (no files scope).\n`);
173
+ }
174
+ console.log(`${claimed.size}/${tree.length} files are claimed by at least one files-scoped block.\n`);
175
+ if (warnings.length === 0) {
176
+ console.log('No scoping warnings.');
177
+ }
178
+ else {
179
+ console.log(`${warnings.length} warning(s):\n`);
180
+ for (const warning of warnings)
181
+ console.log(` ⚠ ${warning}\n`);
182
+ }
183
+ process.exit(args.strict && warnings.length > 0 ? 1 : 0);
184
+ //# sourceMappingURL=scope-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-report.js","sourceRoot":"","sources":["../../scripts/scope-report.ts"],"names":[],"mappings":"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"}
package/kiro/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Kiro CLI enforcement assets
2
+
3
+ Run this framework under **Kiro CLI** with the same Probity guardrail the
4
+ Claude Code integration gets. Probity has no native Kiro vendor, so these
5
+ files translate between Kiro's hook contract and Probity's `claude-code`
6
+ contract.
7
+
8
+ | File | Role |
9
+ |------|------|
10
+ | `probity-kiro.sh` | preToolUse shim (event translation + deny→exit 2) |
11
+ | `probity-kiro-translate.py` | `event` / `reason` translation helpers |
12
+ | `kiro-transcript-to-claude.py` | Kiro session JSONL → Anthropic JSONL (for history rules) |
13
+ | `skill-activation-forced-eval.sh` | userPromptSubmit skill-activation hook |
14
+ | `kiro-agent.template.json` | agent config template (skills + both hooks) |
15
+
16
+ **Setup:** see [`docs/kiro-setup.md`](../../../docs/kiro-setup.md).
17
+
18
+ These are copy-in artifacts (like the `probity.config.*.ts` presets): install
19
+ them into a project's `.kiro/`. This directory is the single source of truth —
20
+ fix Kiro-integration issues here, then re-copy.
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/kiro-agent.json",
3
+ "name": "REPLACE_WITH_AGENT_NAME",
4
+ "description": "REPLACE: Workspace agent that surfaces the agent-skills library (via the .kiro/skills symlink) and enforces this project's Probity rules on every write/shell through the Kiro preToolUse shim.",
5
+ "prompt": "You are the default agent for this workspace. Treat AGENTS.md as the highest-priority context after the user's direct instructions. Honor the mandatory skill-activation sequence injected on every user prompt before implementing. Probity enforces this project's spec-first / test-first workflow mechanically as a preToolUse hook.",
6
+ "tools": [
7
+ "*"
8
+ ],
9
+ "toolsSettings": {
10
+ "read": {
11
+ "allowedPaths": [
12
+ "./**"
13
+ ]
14
+ },
15
+ "write": {
16
+ "allowedPaths": [
17
+ "./**"
18
+ ]
19
+ }
20
+ },
21
+ "resources": [
22
+ "file://AGENTS.md",
23
+ "file://docs/GLOSSARY.md",
24
+ "skill://.kiro/skills/**/SKILL.md"
25
+ ],
26
+ "hooks": {
27
+ "userPromptSubmit": [
28
+ {
29
+ "command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/skill-activation-forced-eval.sh\""
30
+ }
31
+ ],
32
+ "preToolUse": [
33
+ {
34
+ "matcher": "write",
35
+ "command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/probity-kiro.sh\"",
36
+ "timeout_ms": 120000
37
+ },
38
+ {
39
+ "matcher": "shell",
40
+ "command": "bash \"$( (git rev-parse --show-toplevel 2>/dev/null || echo \"$PWD\") )/.kiro/hooks/probity-kiro.sh\"",
41
+ "timeout_ms": 120000
42
+ }
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Kiro session transcript -> Anthropic (Claude Code) transcript transducer.
4
+
5
+ Probity's claude-code vendor reconstructs conversation history from a
6
+ JSONL transcript in Anthropic's persisted message shape (see
7
+ node_modules/@nizos/probity/dist/vendors/claude-code/transcript.js):
8
+
9
+ {"type":"assistant","message":{"content":[
10
+ {"type":"tool_use","id":<id>,"name":<ClaudeToolName>,"input":{...}}]}}
11
+ {"type":"user","message":{"content":[
12
+ {"type":"tool_result","tool_use_id":<id>,"content":<string|blocks>}]}}
13
+ {"type":"user","message":{"content":[{"type":"text","text":<prompt>}]}}
14
+
15
+ Kiro persists a different, versioned event stream at
16
+ ~/.kiro/sessions/cli/<KIRO_SESSION_ID>.jsonl:
17
+
18
+ {"kind":"Prompt","data":{"content":[{"kind":"text","data":<str>}]}}
19
+ {"kind":"AssistantMessage","data":{"content":[
20
+ {"kind":"toolUse","data":{"toolUseId","name","input"}},
21
+ {"kind":"text","data":<str>}, {"kind":"thinking",...}]}}
22
+ {"kind":"ToolResults","data":{"content":[
23
+ {"kind":"toolResult","data":{"toolUseId","content":[{"kind":"text","data"}],"status"}}]}}
24
+
25
+ This script maps the former onto the latter so Probity's history-based
26
+ rules (the commit green-gate `requireGreenTestRun`, the TDD RED-before-GREEN
27
+ judge, characterization/probe resolution) can see prior tool calls and their
28
+ output when running under Kiro.
29
+
30
+ Tool-name mapping matters: Probity's history `toCanonical` only treats a
31
+ Claude `Bash` action as a shell command and `Write`/`Edit` as a file write.
32
+ So Kiro's `shell` must become `Bash` (or the green-gate never sees the
33
+ `xcodebuild ... test` output), and Kiro's `write` becomes `Write`/`Edit`.
34
+
35
+ Reads Kiro JSONL from argv[1] (or stdin) and writes Claude JSONL to stdout.
36
+ Malformed lines are skipped, mirroring Probity's own tolerant reader.
37
+ """
38
+ import json
39
+ import sys
40
+
41
+
42
+ def _text_blocks(content):
43
+ """Join the payloads of a Kiro content array into one string.
44
+
45
+ Text blocks (`kind == "text"`) carry prompts and assistant prose. Tool
46
+ RESULTS, however, are persisted as `kind == "json"` blocks — the shell
47
+ tool's {exit_status, stdout, stderr} object, write confirmations, etc.
48
+ The history-based rules match on command OUTPUT (the green-gate looks for
49
+ a passing test run; the characterization gate looks for the marked test
50
+ failing), so those json payloads must be serialized in rather than
51
+ dropped — otherwise every command's output is invisible under Kiro and
52
+ the gates can never see a test run at all.
53
+ """
54
+ out = []
55
+ for item in content or []:
56
+ if not isinstance(item, dict):
57
+ continue
58
+ kind = item.get("kind")
59
+ data = item.get("data")
60
+ if kind == "text":
61
+ if isinstance(data, str):
62
+ out.append(data)
63
+ elif kind == "json":
64
+ if isinstance(data, str):
65
+ out.append(data)
66
+ else:
67
+ try:
68
+ out.append(json.dumps(data))
69
+ except (TypeError, ValueError):
70
+ pass
71
+ return "\n".join(out)
72
+
73
+
74
+ def _map_tool(name, tool_input):
75
+ """Map a Kiro tool name+input to a Claude (tool_name, input) pair.
76
+
77
+ Only the shapes Probity's history reader interprets need to be exact:
78
+ - shell -> Bash {command}
79
+ - write -> Write {file_path, content} / Edit {file_path, new_string}
80
+ Everything else is passed through under its own name and read by
81
+ Probity as an inert `other` action (no rule matches it).
82
+ """
83
+ tool_input = tool_input if isinstance(tool_input, dict) else {}
84
+ if name in ("shell", "execute_bash", "execute_cmd"):
85
+ return "Bash", {"command": tool_input.get("command", "")}
86
+ if name in ("write", "fs_write", "fsWrite"):
87
+ op = tool_input.get("command")
88
+ path = tool_input.get("path", "")
89
+ if op == "strReplace":
90
+ return "Edit", {
91
+ "file_path": path,
92
+ "old_string": tool_input.get("oldStr", ""),
93
+ "new_string": tool_input.get("newStr", ""),
94
+ "replace_all": bool(tool_input.get("replaceAll", False)),
95
+ }
96
+ # create / insert / append: the introduced text is `content`.
97
+ return "Write", {
98
+ "file_path": path,
99
+ "content": tool_input.get("content", ""),
100
+ }
101
+ return name or "Unknown", tool_input
102
+
103
+
104
+ def transduce_line(raw, emit):
105
+ line = raw.strip()
106
+ if not line:
107
+ return
108
+ try:
109
+ entry = json.loads(line)
110
+ except (ValueError, TypeError):
111
+ return
112
+ if not isinstance(entry, dict):
113
+ return
114
+ kind = entry.get("kind")
115
+ data = entry.get("data") or {}
116
+ content = data.get("content") if isinstance(data, dict) else None
117
+
118
+ if kind == "Prompt":
119
+ text = _text_blocks(content)
120
+ if text:
121
+ emit({"type": "user",
122
+ "message": {"content": [{"type": "text", "text": text}]}})
123
+ return
124
+
125
+ if kind == "AssistantMessage":
126
+ blocks = []
127
+ for item in content or []:
128
+ if not isinstance(item, dict):
129
+ continue
130
+ ikind = item.get("kind")
131
+ idata = item.get("data") or {}
132
+ if ikind == "toolUse" and isinstance(idata, dict):
133
+ cname, cinput = _map_tool(idata.get("name"), idata.get("input"))
134
+ blocks.append({
135
+ "type": "tool_use",
136
+ "id": idata.get("toolUseId", ""),
137
+ "name": cname,
138
+ "input": cinput,
139
+ })
140
+ elif ikind == "text" and isinstance(idata, str):
141
+ blocks.append({"type": "text", "text": idata})
142
+ if blocks:
143
+ emit({"type": "assistant", "message": {"content": blocks}})
144
+ return
145
+
146
+ if kind == "ToolResults":
147
+ for item in content or []:
148
+ if not isinstance(item, dict) or item.get("kind") != "toolResult":
149
+ continue
150
+ idata = item.get("data") or {}
151
+ if not isinstance(idata, dict):
152
+ continue
153
+ emit({
154
+ "type": "user",
155
+ "message": {"content": [{
156
+ "type": "tool_result",
157
+ "tool_use_id": idata.get("toolUseId", ""),
158
+ "content": _text_blocks(idata.get("content")),
159
+ }]},
160
+ })
161
+ return
162
+ # Unknown top-level kinds are ignored.
163
+
164
+
165
+ def main(argv):
166
+ src = open(argv[1], "r", encoding="utf-8") if len(argv) > 1 else sys.stdin
167
+ out = sys.stdout
168
+
169
+ def emit(obj):
170
+ out.write(json.dumps(obj))
171
+ out.write("\n")
172
+
173
+ try:
174
+ for raw in src:
175
+ transduce_line(raw, emit)
176
+ finally:
177
+ if src is not sys.stdin:
178
+ src.close()
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ sys.exit(main(sys.argv))