safeword 0.61.0 → 0.62.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 (43) hide show
  1. package/dist/{check-KHWH36AK.js → check-FH7FC7IG.js} +3 -3
  2. package/dist/{chunk-XTGWCHWI.js → chunk-2IK7L4EZ.js} +2 -2
  3. package/dist/{chunk-U2GSR4OM.js → chunk-4DUNUF4B.js} +110 -6
  4. package/dist/chunk-4DUNUF4B.js.map +1 -0
  5. package/dist/{chunk-C4LNUGHF.js → chunk-AA3G2VTE.js} +2 -2
  6. package/dist/{chunk-GEC7AHRF.js → chunk-G7IU3XBI.js} +61 -6
  7. package/dist/chunk-G7IU3XBI.js.map +1 -0
  8. package/dist/cli.js +6 -6
  9. package/dist/{diff-K3SYBGB7.js → diff-LLE62QKI.js} +2 -2
  10. package/dist/{github-rest-36XJI5HD.js → github-rest-3BZDDOX6.js} +6 -5
  11. package/dist/github-rest-3BZDDOX6.js.map +1 -0
  12. package/dist/{reset-4IGPE6TD.js → reset-NBZGOP4I.js} +2 -2
  13. package/dist/{retro-FCGFUDEA.js → retro-ART3JCCW.js} +107 -40
  14. package/dist/retro-ART3JCCW.js.map +1 -0
  15. package/dist/{retro-extract-XXZI3DFW.js → retro-extract-6IZLY25L.js} +12 -2
  16. package/dist/{setup-SECXSZ5K.js → setup-46AW3EOX.js} +4 -4
  17. package/dist/{upgrade-S5DTXDLC.js → upgrade-JELXQM56.js} +4 -4
  18. package/package.json +1 -1
  19. package/templates/codex/config.toml +16 -8
  20. package/templates/commands/audit.md +1 -397
  21. package/templates/commands/retro.md +5 -0
  22. package/templates/hooks/codex/stop.ts +72 -32
  23. package/templates/hooks/cursor/after-file-edit.ts +8 -5
  24. package/templates/hooks/cursor/before-shell-execution.ts +5 -0
  25. package/templates/hooks/cursor/gate-adapter.ts +4 -0
  26. package/templates/hooks/cursor/stop.ts +3 -3
  27. package/templates/hooks/lib/cursor-state.ts +59 -0
  28. package/templates/hooks/lib/retro-extract.ts +188 -11
  29. package/templates/hooks/write-review-stamp.ts +35 -6
  30. package/templates/skills/audit/SKILL.md +9 -9
  31. package/templates/skills/retro/SKILL.md +104 -0
  32. package/dist/chunk-GEC7AHRF.js.map +0 -1
  33. package/dist/chunk-U2GSR4OM.js.map +0 -1
  34. package/dist/github-rest-36XJI5HD.js.map +0 -1
  35. package/dist/retro-FCGFUDEA.js.map +0 -1
  36. /package/dist/{check-KHWH36AK.js.map → check-FH7FC7IG.js.map} +0 -0
  37. /package/dist/{chunk-XTGWCHWI.js.map → chunk-2IK7L4EZ.js.map} +0 -0
  38. /package/dist/{chunk-C4LNUGHF.js.map → chunk-AA3G2VTE.js.map} +0 -0
  39. /package/dist/{diff-K3SYBGB7.js.map → diff-LLE62QKI.js.map} +0 -0
  40. /package/dist/{reset-4IGPE6TD.js.map → reset-NBZGOP4I.js.map} +0 -0
  41. /package/dist/{retro-extract-XXZI3DFW.js.map → retro-extract-6IZLY25L.js.map} +0 -0
  42. /package/dist/{setup-SECXSZ5K.js.map → setup-46AW3EOX.js.map} +0 -0
  43. /package/dist/{upgrade-S5DTXDLC.js.map → upgrade-JELXQM56.js.map} +0 -0
@@ -4,14 +4,14 @@ import {
4
4
  import {
5
5
  checkHealth,
6
6
  reportHealthSummary
7
- } from "./chunk-XTGWCHWI.js";
7
+ } from "./chunk-2IK7L4EZ.js";
8
8
  import "./chunk-HWOJR33A.js";
9
9
  import "./chunk-XTLCJKGE.js";
10
10
  import {
11
11
  fetchRegistryLatestVersion,
12
12
  isNewerVersion
13
13
  } from "./chunk-LRYWFRPD.js";
14
- import "./chunk-GEC7AHRF.js";
14
+ import "./chunk-G7IU3XBI.js";
15
15
  import {
16
16
  detectPackageManager
17
17
  } from "./chunk-OY7F4YNQ.js";
@@ -125,4 +125,4 @@ async function check(options) {
125
125
  export {
126
126
  check
127
127
  };
128
- //# sourceMappingURL=check-KHWH36AK.js.map
128
+ //# sourceMappingURL=check-FH7FC7IG.js.map
@@ -15,7 +15,7 @@ import {
15
15
  createProjectContext,
16
16
  getMissingPacks,
17
17
  reconcile
18
- } from "./chunk-GEC7AHRF.js";
18
+ } from "./chunk-G7IU3XBI.js";
19
19
  import {
20
20
  findDanglingDependencies,
21
21
  findTicketsInCycles,
@@ -747,4 +747,4 @@ export {
747
747
  checkHealth,
748
748
  reportHealthSummary
749
749
  };
750
- //# sourceMappingURL=chunk-XTGWCHWI.js.map
750
+ //# sourceMappingURL=chunk-2IK7L4EZ.js.map
@@ -3,18 +3,23 @@ import {
3
3
  } from "./chunk-B6KVKEZA.js";
4
4
 
5
5
  // templates/hooks/lib/retro-extract.ts
6
- import { readFileSync } from "fs";
6
+ import { readFileSync, writeFileSync } from "fs";
7
7
  import nodePath from "path";
8
8
  var DIGEST_CAP = 18e4;
9
- var DEFAULT_RETRO_MODEL = "sonnet";
10
- function resolveRetroModel(projectDirectory) {
9
+ var DEFAULT_CLAUDE_RETRO_MODEL = "sonnet";
10
+ var DEFAULT_CODEX_RETRO_MODEL = "gpt-5.5";
11
+ var DEFAULT_RETRO_MODEL = DEFAULT_CLAUDE_RETRO_MODEL;
12
+ function defaultRetroModel(agent) {
13
+ return agent === "codex" ? DEFAULT_CODEX_RETRO_MODEL : DEFAULT_CLAUDE_RETRO_MODEL;
14
+ }
15
+ function resolveRetroModel(projectDirectory, agent = "claude") {
11
16
  try {
12
17
  const raw = readFileSync(nodePath.join(projectDirectory, ".safeword", "config.json"), "utf8");
13
18
  const parsed = JSON.parse(raw);
14
19
  const model = parsed.retro?.model;
15
- return typeof model === "string" && model.length > 0 ? model : DEFAULT_RETRO_MODEL;
20
+ return typeof model === "string" && model.length > 0 ? model : defaultRetroModel(agent);
16
21
  } catch {
17
- return DEFAULT_RETRO_MODEL;
22
+ return defaultRetroModel(agent);
18
23
  }
19
24
  }
20
25
  var OVERLAP_CHARS = 2048;
@@ -54,10 +59,68 @@ function buildExtractArgv(options) {
54
59
  options.prompt
55
60
  ];
56
61
  }
62
+ function buildCodexExtractArgv(options) {
63
+ return [
64
+ "exec",
65
+ "--ignore-user-config",
66
+ "--disable",
67
+ "hooks",
68
+ "-c",
69
+ "mcp_servers={}",
70
+ "--output-schema",
71
+ options.schemaPath,
72
+ "-o",
73
+ options.outputPath,
74
+ "--json",
75
+ "--sandbox",
76
+ "read-only",
77
+ "-m",
78
+ options.model,
79
+ options.prompt
80
+ ];
81
+ }
57
82
  var EXTRACT_SYSTEM_PROMPT = `You extract SAFEWORD's OWN friction from a session digest. Output ONLY a JSON array (no prose). Each item: {"category":"bug|rough-edge|gap","title":"canonical title of the SAFEWORD behavior","safeword_surface":"a real safeword path: hooks/\u2026, packages/cli/\u2026, templates/\u2026, dist/\u2026, or .safeword/\u2026","what_happened":"","why_friction":"","repro":"in terms of safeword commands"}. Rules: SAFEWORD's friction only (not the host project, not Claude Code itself); canonical behavior-titles; do not invent; [] if none.`;
83
+ var CODEX_RETRO_OUTPUT_SCHEMA = {
84
+ type: "object",
85
+ additionalProperties: false,
86
+ properties: {
87
+ findings: {
88
+ type: "array",
89
+ items: {
90
+ type: "object",
91
+ additionalProperties: false,
92
+ properties: {
93
+ category: { type: "string", enum: ["bug", "rough-edge", "gap"] },
94
+ title: { type: "string" },
95
+ safeword_surface: { type: "string" },
96
+ what_happened: { type: "string" },
97
+ why_friction: { type: "string" },
98
+ repro: { type: "string" }
99
+ },
100
+ required: [
101
+ "category",
102
+ "title",
103
+ "safeword_surface",
104
+ "what_happened",
105
+ "why_friction",
106
+ "repro"
107
+ ]
108
+ }
109
+ }
110
+ },
111
+ required: ["findings"]
112
+ };
58
113
  function buildExtractPrompt(digestPath) {
59
114
  return `Read the file ${digestPath} and extract SAFEWORD's own friction as the JSON array described. Output only the JSON array.`;
60
115
  }
116
+ function buildCodexExtractPrompt(digest) {
117
+ return `${EXTRACT_SYSTEM_PROMPT}
118
+
119
+ Return only JSON matching the provided output schema: {"findings":[...]}. Use an empty findings array when there is no safeword friction.
120
+
121
+ Transcript digest:
122
+ ${digest}`;
123
+ }
61
124
  function parseFindings(stdout) {
62
125
  let envelopeResult;
63
126
  try {
@@ -76,6 +139,14 @@ function parseFindings(stdout) {
76
139
  return [];
77
140
  }
78
141
  }
142
+ function parseCodexFindingsOutput(rawOutput) {
143
+ try {
144
+ const parsed = JSON.parse(rawOutput);
145
+ return Array.isArray(parsed.findings) ? parsed.findings : void 0;
146
+ } catch {
147
+ return void 0;
148
+ }
149
+ }
79
150
  async function runHeadlessExtraction(transcript, dependencies) {
80
151
  try {
81
152
  const digestPath = dependencies.writeDigest(buildDigest(transcript));
@@ -94,6 +165,34 @@ async function runHeadlessExtraction(transcript, dependencies) {
94
165
  return [];
95
166
  }
96
167
  }
168
+ async function runCodexHeadlessExtractionChecked(transcript, dependencies) {
169
+ try {
170
+ const schemaPath = dependencies.schemaPath ?? nodePath.join(dependencies.cwd, "schema.json");
171
+ const outputPath = dependencies.outputPath ?? nodePath.join(dependencies.cwd, "output.json");
172
+ const writeFile = dependencies.writeFile ?? writeFileSync;
173
+ const readFile = dependencies.readFile ?? ((path) => readFileSync(path, "utf8"));
174
+ writeFile(schemaPath, JSON.stringify(CODEX_RETRO_OUTPUT_SCHEMA));
175
+ const argv = buildCodexExtractArgv({
176
+ model: dependencies.model ?? DEFAULT_CODEX_RETRO_MODEL,
177
+ schemaPath,
178
+ outputPath,
179
+ prompt: buildCodexExtractPrompt(buildDigest(transcript))
180
+ });
181
+ const { code } = await dependencies.spawn(argv, {
182
+ cwd: dependencies.cwd,
183
+ env: { ...dependencies.env, [RETRO_CHILD_ENV]: "1" },
184
+ stdio: "ignore"
185
+ });
186
+ if (code !== 0) return { ok: false, findings: [] };
187
+ const findings = parseCodexFindingsOutput(readFile(outputPath));
188
+ return findings === void 0 ? { ok: false, findings: [] } : { ok: true, findings };
189
+ } catch {
190
+ return { ok: false, findings: [] };
191
+ }
192
+ }
193
+ async function runCodexHeadlessExtraction(transcript, dependencies) {
194
+ return (await runCodexHeadlessExtractionChecked(transcript, dependencies)).findings;
195
+ }
97
196
  var SHORT_RESULT = 600;
98
197
  var TOOL_USE_INPUT_CAP = 300;
99
198
  var FRICTION = /error|fail|block|denied|stale|drift|guard|FAILED/i;
@@ -129,6 +228,8 @@ function buildDigest(rawTranscript, cap = DIGEST_CAP) {
129
228
 
130
229
  export {
131
230
  DIGEST_CAP,
231
+ DEFAULT_CLAUDE_RETRO_MODEL,
232
+ DEFAULT_CODEX_RETRO_MODEL,
132
233
  DEFAULT_RETRO_MODEL,
133
234
  resolveRetroModel,
134
235
  OVERLAP_CHARS,
@@ -138,7 +239,10 @@ export {
138
239
  isRetroChild,
139
240
  RETRO_EXTRACT_CMD_ENV,
140
241
  buildExtractArgv,
242
+ buildCodexExtractArgv,
141
243
  runHeadlessExtraction,
244
+ runCodexHeadlessExtractionChecked,
245
+ runCodexHeadlessExtraction,
142
246
  buildDigest
143
247
  };
144
- //# sourceMappingURL=chunk-U2GSR4OM.js.map
248
+ //# sourceMappingURL=chunk-4DUNUF4B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../templates/hooks/lib/retro-extract.ts"],"sourcesContent":["// Safeword: retro headless-extraction helpers (ticket 7D8PJP).\n//\n// The invisible-retro path runs extraction in a separate headless `claude -p`\n// session launched by the Stop hook — never injected into the user's\n// conversation. This module holds the pieces that path needs: a transcript\n// DIGEST (transcripts are multi-MB, far past any model context), and — added in\n// later steps — the headless argv builder, the recursion-guard predicate, and the\n// synchronous runner. Agent-neutral where possible; Claude-specific bits are\n// named as such.\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { iterateJsonlEntries } from './jsonl-spool.js';\n\n/**\n * Default cap (chars) for a transcript digest. A real session transcript is tens\n * of MB; the extractor needs a bounded, signal-dense slice, not the raw JSONL.\n */\nexport const DIGEST_CAP = 180_000;\n\n/**\n * Default Claude extraction model. Sonnet, not haiku: measured head-to-head on\n * the same transcript, haiku surfaced 1–3 weak findings vs sonnet's 9 strong\n * ones (ZFGWS1). Overridable per install via `.safeword/config.json` →\n * `retro.model`.\n */\nexport const DEFAULT_CLAUDE_RETRO_MODEL = 'sonnet';\n\n/** Default Codex extraction model. Stock Codex can only call OpenAI models. */\nexport const DEFAULT_CODEX_RETRO_MODEL = 'gpt-5.5';\n\n/** Backward-compatible alias for existing Claude call sites/tests. */\nexport const DEFAULT_RETRO_MODEL = DEFAULT_CLAUDE_RETRO_MODEL;\n\nexport type RetroAgent = 'claude' | 'codex';\n\nfunction defaultRetroModel(agent: RetroAgent): string {\n return agent === 'codex' ? DEFAULT_CODEX_RETRO_MODEL : DEFAULT_CLAUDE_RETRO_MODEL;\n}\n\n/**\n * Resolve the extraction model for an install: `retro.model` from\n * `.safeword/config.json`, else the per-agent default. Fail-open to the default\n * on any missing/unreadable/malformed config — model selection must never break\n * the out-of-band retro run.\n */\nexport function resolveRetroModel(projectDirectory: string, agent: RetroAgent = 'claude'): string {\n try {\n const raw = readFileSync(nodePath.join(projectDirectory, '.safeword', 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { retro?: { model?: unknown } };\n const model = parsed.retro?.model;\n return typeof model === 'string' && model.length > 0 ? model : defaultRetroModel(agent);\n } catch {\n return defaultRetroModel(agent);\n }\n}\n\n/**\n * Overlap (chars) re-included before a delta window's start, so a finding\n * straddling a window boundary appears whole in one fire (ticket ZFGWS1). A\n * char-slice may cut the first JSONL line; `buildDigest` skips that malformed head\n * line, so whole boundary entries still survive. Duplicate findings from the\n * overlap are absorbed by signature dedupe (triage).\n */\nexport const OVERLAP_CHARS = 2048;\n\n/**\n * Slice the delta window a fire should digest: from `windowStart` (the previous\n * fire's recorded offset) minus a small overlap, to the end. The FIRST fire\n * (`windowStart <= 0`) returns the whole transcript so far. So `buildDigest`'s cap\n * applies to the WINDOW, not the chronological head — defeating the head-cap that\n * made plain re-arm inert. The overlap is clamped at the start of the transcript.\n */\nexport function windowFor(\n transcript: string,\n windowStart: number,\n overlap: number = OVERLAP_CHARS,\n): string {\n if (windowStart <= 0) return transcript;\n return transcript.slice(Math.max(0, windowStart - overlap));\n}\n\n/** A decision to run the retro child, carrying the delta window + stable session id. */\nexport interface RetroChildInvocation {\n transcriptPath: string;\n windowStart: number;\n sessionId: string;\n}\n\n/**\n * Build the `safeword retro` argv the Stop hook spawns out-of-band. Forwards the\n * delta window offset and the resolved session id (ZFGWS1) so the child digests\n * only the new window and attributes findings to the real session — not the\n * 'unknown' fallback its own env resolves to in cloud.\n */\nexport function retroChildArgs(invocation: RetroChildInvocation): string[] {\n return [\n 'retro',\n '--auto-extract',\n '--transcript',\n invocation.transcriptPath,\n '--window-start',\n String(invocation.windowStart),\n '--session-id',\n invocation.sessionId,\n ];\n}\n\n/**\n * The headless extractor only ever READS the digest — never writes, edits, or\n * runs shell. A read-only allow-list keeps the out-of-band child from mutating\n * the working tree or exfiltrating via a tool.\n */\nconst READ_ONLY_TOOLS = 'Read';\n\n/**\n * Env sentinel set on the headless child. The auth-working invocation does NOT\n * use `--bare`, so the child loads safeword's hooks; the retro Stop hook checks\n * this and early-returns, so the child can't re-trigger retro (infinite spawn) —\n * and stop-retro is the only hook that recursively spawns `claude -p`, so guarding\n * it is sufficient. NOT `--bare` (breaks cloud auth) and NOT `CLAUDE_CODE_CHILD_SESSION`\n * (already `1` in the normal tool context, so it can't distinguish a retro child).\n */\nexport const RETRO_CHILD_ENV = 'SAFEWORD_RETRO_CHILD';\n\n/** Whether the current process is a retro headless child (recursion guard). */\nexport function isRetroChild(env: Record<string, string | undefined>): boolean {\n return (env[RETRO_CHILD_ENV] ?? '').length > 0;\n}\n\n/**\n * Override for the command the Stop hook spawns to run the extraction CLI. When\n * set, the hook spawns this command verbatim instead of resolving `safeword retro\n * --auto-extract`. A test/advanced seam so the hook's invisibility can be proven\n * without launching a real headless `claude -p`.\n */\nexport const RETRO_EXTRACT_CMD_ENV = 'SAFEWORD_RETRO_EXTRACT_CMD';\n\nexport interface ExtractArgvOptions {\n /** Model for the headless extraction (cheap by default — see the caller). */\n model: string;\n /** System prompt appended to the extractor's default behavior. */\n systemPrompt: string;\n /** The task prompt (trailing positional) — instructs reading the digest. */\n prompt: string;\n}\n\n/**\n * Build the `claude` argv for a headless, isolated retro extraction. Print mode\n * + JSON output, a read-only tool set, and — load-bearing — NO `--bare`: `--bare`\n * skips the managed-provider setup a Claude cloud container authenticates through\n * (proven live: `--bare` → Authentication error). The returned array excludes the\n * `claude` binary itself; the runner supplies it.\n */\nexport function buildExtractArgv(options: ExtractArgvOptions): string[] {\n return [\n '-p',\n '--model',\n options.model,\n '--allowed-tools',\n READ_ONLY_TOOLS,\n '--output-format',\n 'json',\n '--append-system-prompt',\n options.systemPrompt,\n options.prompt,\n ];\n}\n\nexport interface CodexExtractArgvOptions {\n /** OpenAI model for the child `codex exec` process. */\n model: string;\n /** JSON schema path supplied to `--output-schema`. */\n schemaPath: string;\n /** JSON output path supplied to `-o`. */\n outputPath: string;\n /** Inline digest + task prompt. No Read/MCP tools are available. */\n prompt: string;\n}\n\n/**\n * Build the `codex exec` argv for a headless retro extraction. The digest is\n * inline in the prompt because `--output-schema` is ignored by Codex when the\n * task requires Read/MCP tool use. Hooks and MCP servers are explicitly disabled,\n * stdin is closed by the runner, and the sandbox is read-only.\n */\nexport function buildCodexExtractArgv(options: CodexExtractArgvOptions): string[] {\n return [\n 'exec',\n '--ignore-user-config',\n '--disable',\n 'hooks',\n '-c',\n 'mcp_servers={}',\n '--output-schema',\n options.schemaPath,\n '-o',\n options.outputPath,\n '--json',\n '--sandbox',\n 'read-only',\n '-m',\n options.model,\n options.prompt,\n ];\n}\n\n// The extraction rules, mirrored from templates/guides/retro.md: SAFEWORD's own\n// friction only, the constrained snake_case schema, no invention. The egress\n// guard sanitizes downstream, so the child writes plainly.\nconst EXTRACT_SYSTEM_PROMPT =\n \"You extract SAFEWORD's OWN friction from a session digest. Output ONLY a JSON \" +\n 'array (no prose). Each item: {\"category\":\"bug|rough-edge|gap\",\"title\":\"canonical ' +\n 'title of the SAFEWORD behavior\",\"safeword_surface\":\"a real safeword path: ' +\n 'hooks/…, packages/cli/…, templates/…, dist/…, or .safeword/…\",\"what_happened\":\"\",' +\n '\"why_friction\":\"\",\"repro\":\"in terms of safeword commands\"}. Rules: SAFEWORD\\'s ' +\n 'friction only (not the host project, not Claude Code itself); canonical ' +\n 'behavior-titles; do not invent; [] if none.';\n\nconst CODEX_RETRO_OUTPUT_SCHEMA = {\n type: 'object',\n additionalProperties: false,\n properties: {\n findings: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n category: { type: 'string', enum: ['bug', 'rough-edge', 'gap'] },\n title: { type: 'string' },\n safeword_surface: { type: 'string' },\n what_happened: { type: 'string' },\n why_friction: { type: 'string' },\n repro: { type: 'string' },\n },\n required: [\n 'category',\n 'title',\n 'safeword_surface',\n 'what_happened',\n 'why_friction',\n 'repro',\n ],\n },\n },\n },\n required: ['findings'],\n};\n\n/** The task prompt: point the read-only child at the digest file. */\nfunction buildExtractPrompt(digestPath: string): string {\n return `Read the file ${digestPath} and extract SAFEWORD's own friction as the JSON array described. Output only the JSON array.`;\n}\n\nfunction buildCodexExtractPrompt(digest: string): string {\n return (\n `${EXTRACT_SYSTEM_PROMPT}\\n\\n` +\n 'Return only JSON matching the provided output schema: {\"findings\":[...]}. ' +\n 'Use an empty findings array when there is no safeword friction.\\n\\n' +\n `Transcript digest:\\n${digest}`\n );\n}\n\n/** Parse a findings array out of a `claude -p --output-format json` envelope. */\nfunction parseFindings(stdout: string): unknown[] {\n let envelopeResult: string;\n try {\n const parsed = JSON.parse(stdout) as { result?: unknown };\n if (typeof parsed.result !== 'string') return [];\n envelopeResult = parsed.result;\n } catch {\n return [];\n }\n // The model's text may be fenced (```json … ```); pull the first array literal.\n const match = envelopeResult.match(/\\[[\\S\\s]*\\]/);\n if (!match) return [];\n try {\n const findings = JSON.parse(match[0]) as unknown;\n return Array.isArray(findings) ? findings : [];\n } catch {\n return [];\n }\n}\n\nfunction parseCodexFindingsOutput(rawOutput: string): unknown[] | undefined {\n try {\n const parsed = JSON.parse(rawOutput) as { findings?: unknown };\n return Array.isArray(parsed.findings) ? parsed.findings : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Result of spawning the headless extractor. */\nexport interface SpawnResult {\n code: number | null;\n stdout: string;\n}\n\n/** Dependencies for `runHeadlessExtraction` — the process boundaries, injected. */\nexport interface RunExtractionDeps {\n /** Spawn `claude` with argv; resolve with exit code + stdout. Awaited (sync). */\n spawn: (\n argv: string[],\n options: { cwd: string; env: Record<string, string | undefined> },\n ) => Promise<SpawnResult>;\n /** Persist the digest to a file the read-only child can Read; return its path. */\n writeDigest: (digest: string) => string;\n /** Base env for the child (the sentinel is added here). */\n env: Record<string, string | undefined>;\n /** Neutral cwd — NOT the user's project — so project hooks don't load. */\n cwd: string;\n /** Extraction model (cheap by default). */\n model?: string;\n}\n\nexport interface CodexSpawnOptions {\n cwd: string;\n env: Record<string, string | undefined>;\n stdio: 'ignore';\n}\n\n/** Dependencies for `runCodexHeadlessExtraction` — injected for tests. */\nexport interface RunCodexExtractionDeps {\n /** Spawn `codex` with argv; resolve after the child exits. Awaited (sync). */\n spawn: (argv: string[], options: CodexSpawnOptions) => Promise<SpawnResult>;\n /** Persist the output schema file read by `codex exec --output-schema`. */\n writeFile?: (path: string, content: string) => void;\n /** Read the `-o` JSON file written by `codex exec`. */\n readFile?: (path: string) => string;\n /** Base env for the child (the sentinel is added here). */\n env: Record<string, string | undefined>;\n /** Neutral cwd — NOT the user's project — so project hooks don't load. */\n cwd: string;\n /** Extraction model (OpenAI default for Codex). */\n model?: string;\n /** Test seam for deterministic schema/output paths. */\n schemaPath?: string;\n outputPath?: string;\n}\n\nexport interface CodexExtractionResult {\n /** True only when the child exited successfully and wrote schema-valid JSON. */\n ok: boolean;\n findings: unknown[];\n}\n\n/**\n * Run the retro extraction in a separate, isolated headless `claude -p` session\n * and return the raw findings array. Synchronous (awaits the spawn) and\n * fail-OPEN: any error — non-zero exit, unparseable output, a spawn throw —\n * yields `[]` and never throws, so the Stop hook that wraps this stays silent and\n * never blocks. Spawn contract: the digest is the input (referenced in the\n * prompt), the child runs from the neutral cwd, and the child env carries\n * `SAFEWORD_RETRO_CHILD=1` (recursion guard).\n */\nexport async function runHeadlessExtraction(\n transcript: string,\n dependencies: RunExtractionDeps,\n): Promise<unknown[]> {\n try {\n const digestPath = dependencies.writeDigest(buildDigest(transcript));\n const argv = buildExtractArgv({\n model: dependencies.model ?? DEFAULT_RETRO_MODEL,\n systemPrompt: EXTRACT_SYSTEM_PROMPT,\n prompt: buildExtractPrompt(digestPath),\n });\n const { code, stdout } = await dependencies.spawn(argv, {\n cwd: dependencies.cwd,\n env: { ...dependencies.env, [RETRO_CHILD_ENV]: '1' },\n });\n if (code !== 0) return []; // fail-open on a failed extraction\n return parseFindings(stdout);\n } catch {\n return []; // fail-open on any spawn/IO error\n }\n}\n\n/**\n * Run Codex retro extraction in a separate `codex exec` child and return the raw\n * findings array with a success bit. Synchronous (awaits the child) and\n * fail-OPEN: any non-zero exit, missing output, malformed JSON, or\n * spawn/read/write error yields `{ ok:false, findings:[] }`.\n */\nexport async function runCodexHeadlessExtractionChecked(\n transcript: string,\n dependencies: RunCodexExtractionDeps,\n): Promise<CodexExtractionResult> {\n try {\n const schemaPath = dependencies.schemaPath ?? nodePath.join(dependencies.cwd, 'schema.json');\n const outputPath = dependencies.outputPath ?? nodePath.join(dependencies.cwd, 'output.json');\n const writeFile = dependencies.writeFile ?? writeFileSync;\n const readFile = dependencies.readFile ?? ((path: string) => readFileSync(path, 'utf8'));\n\n writeFile(schemaPath, JSON.stringify(CODEX_RETRO_OUTPUT_SCHEMA));\n const argv = buildCodexExtractArgv({\n model: dependencies.model ?? DEFAULT_CODEX_RETRO_MODEL,\n schemaPath,\n outputPath,\n prompt: buildCodexExtractPrompt(buildDigest(transcript)),\n });\n const { code } = await dependencies.spawn(argv, {\n cwd: dependencies.cwd,\n env: { ...dependencies.env, [RETRO_CHILD_ENV]: '1' },\n stdio: 'ignore',\n });\n if (code !== 0) return { ok: false, findings: [] };\n const findings = parseCodexFindingsOutput(readFile(outputPath));\n return findings === undefined ? { ok: false, findings: [] } : { ok: true, findings };\n } catch {\n return { ok: false, findings: [] };\n }\n}\n\n/**\n * Backward-compatible fail-open wrapper for call sites that only need findings.\n */\nexport async function runCodexHeadlessExtraction(\n transcript: string,\n dependencies: RunCodexExtractionDeps,\n): Promise<unknown[]> {\n return (await runCodexHeadlessExtractionChecked(transcript, dependencies)).findings;\n}\n\n// A tool-result body is kept whole only when it's short OR carries a friction\n// signal (errors/failures/gate blocks are exactly what retro mines); larger\n// non-signal bodies are dropped so they can't crowd out text + tool-use names.\nconst SHORT_RESULT = 600;\n/** Cap for a tool_use's serialized input in the digest — names it enough, not the whole payload. */\nconst TOOL_USE_INPUT_CAP = 300;\nconst FRICTION = /error|fail|block|denied|stale|drift|guard|FAILED/i;\n\ninterface ContentItem {\n type?: string;\n text?: string;\n name?: string;\n input?: unknown;\n content?: unknown;\n}\n\ninterface TranscriptEntry {\n type?: string;\n message?: { role?: string; content?: ContentItem[] | string };\n}\n\nfunction lineFor(item: ContentItem, role: string): string | undefined {\n if (item.type === 'text' && item.text) return `[${role}] ${item.text}`;\n if (item.type === 'tool_use' && item.name)\n return `[tool_use] ${item.name}: ${JSON.stringify(item.input ?? {}).slice(0, TOOL_USE_INPUT_CAP)}`;\n if (item.type === 'tool_result') {\n const text =\n typeof item.content === 'string' ? item.content : JSON.stringify(item.content ?? '');\n // Keep short results, or longer ones only when they carry a friction signal.\n if (text && (text.length < SHORT_RESULT || FRICTION.test(text)))\n return `[tool_result] ${text.slice(0, SHORT_RESULT)}`;\n }\n return undefined;\n}\n\n/**\n * Reduce a raw JSONL transcript to a signal-dense digest under `cap` chars:\n * user/assistant text + tool-use names + short/error-ish tool results. Oversized\n * non-signal tool-result bodies are omitted (not just truncated), so they can't\n * crowd out the markers the extractor needs. Malformed lines are skipped, never\n * thrown — a hook must not crash on a partial transcript.\n */\nexport function buildDigest(rawTranscript: string, cap: number = DIGEST_CAP): string {\n const out: string[] = [];\n for (const raw of iterateJsonlEntries(rawTranscript)) {\n const entry = raw as TranscriptEntry;\n const role = entry.message?.role ?? entry.type ?? 'entry';\n const content = entry.message?.content;\n if (typeof content === 'string') {\n out.push(`[${role}] ${content}`);\n } else if (Array.isArray(content)) {\n for (const item of content) {\n const rendered = lineFor(item, role);\n if (rendered !== undefined) out.push(rendered);\n }\n }\n }\n const digest = out.join('\\n');\n return digest.length > cap ? digest.slice(0, cap) : digest;\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  detectLanguages,
5
5
  isGitRepo,
6
6
  isPackInstalled
7
- } from "./chunk-GEC7AHRF.js";
7
+ } from "./chunk-G7IU3XBI.js";
8
8
  import {
9
9
  SAFEWORD_PEER_DEPENDENCIES
10
10
  } from "./chunk-HSC7TELY.js";
@@ -546,4 +546,4 @@ export {
546
546
  getEslintPeerMismatchWarning,
547
547
  maybeAutoPatchOrNudge
548
548
  };
549
- //# sourceMappingURL=chunk-C4LNUGHF.js.map
549
+ //# sourceMappingURL=chunk-AA3G2VTE.js.map
@@ -2103,8 +2103,23 @@ ${safewordImport}${prettier.import}
2103
2103
  // in the same setup run, so this fallback only fires in degenerate cases.
2104
2104
  let projectConfig = [];
2105
2105
  const projectConfigPath = new URL("../${existingConfig}", import.meta.url);
2106
+
2107
+ function isTypeScriptProjectConfig(configPath) {
2108
+ return /\\.[cm]?ts$/.test(configPath.pathname);
2109
+ }
2110
+
2111
+ async function loadProjectConfig() {
2112
+ if (isTypeScriptProjectConfig(projectConfigPath)) {
2113
+ const { createJiti } = await import("jiti");
2114
+ const jiti = createJiti(import.meta.url);
2115
+ return await jiti.import(projectConfigPath.href, { default: true });
2116
+ }
2117
+ const loaded = await import("../${existingConfig}");
2118
+ return loaded.default;
2119
+ }
2120
+
2106
2121
  if (existsSync(projectConfigPath)) {
2107
- projectConfig = (await import("../${existingConfig}")).default;
2122
+ projectConfig = await loadProjectConfig();
2108
2123
  if (!Array.isArray(projectConfig)) {
2109
2124
  projectConfig = [projectConfig];
2110
2125
  }
@@ -2635,12 +2650,15 @@ var typescriptJsonMerges = {
2635
2650
  ".oxfmtrc.jsonc": OXFMT_JSON_MERGE
2636
2651
  };
2637
2652
  var ESLINT_PACKAGE = "eslint@^10.0.0";
2653
+ var JITI_PACKAGE = "jiti@^2.2.0";
2638
2654
  var typescriptPackages = {
2639
2655
  base: [
2640
2656
  // Core tools (always needed for JS/TS — eslint/prettier also lint the BDD lane's .ts step files)
2641
2657
  ESLINT_PACKAGE,
2642
2658
  // Safeword (bundles eslint-config-prettier + all ESLint plugins)
2643
2659
  "safeword",
2660
+ // Generated .safeword/eslint.config.mjs uses jiti to load eslint.config.ts.
2661
+ JITI_PACKAGE,
2644
2662
  // BDD acceptance lane (ticket 102b) — cucumber-js runs the scaffolded
2645
2663
  // .feature files; tsx transpiles the TypeScript step definitions, and
2646
2664
  // @types/node lets the scaffolded steps (node: imports) pass typechecks.
@@ -2677,7 +2695,8 @@ var CURSOR_ACTION_SKILLS = [
2677
2695
  "explain",
2678
2696
  "cleanup-zombies",
2679
2697
  "self-review",
2680
- "review-spec"
2698
+ "review-spec",
2699
+ "retro"
2681
2700
  ];
2682
2701
  var DEBUG_DESCRIPTION = "Four-phase debugging framework that ensures root cause identification before fixes. Use when encountering bugs, test failures, unexpected behavior, or when previous fix attempts failed. Enforces investigate-first discipline ('debug this', 'fix this error', 'test is failing', 'not working').";
2683
2702
  var REFACTOR_DESCRIPTION = "Systematic refactoring with small-step discipline. Use when user says 'refactor', 'clean up', 'restructure', 'extract', 'rename', 'simplify', or mentions code smells. Enforces one change \u2192 test \u2192 commit when the commit can stay scoped. For structural improvements, NOT style/formatting (use /lint). NOT for adding features or fixing bugs.";
@@ -2697,11 +2716,21 @@ var CURSOR_COMMAND_WRAPPERS = [
2697
2716
  description: "Deep code review with web research. USE WHEN user says 'double check against latest', 'verify versions', 'check security'. Complements automatic quality hook with ecosystem verification.",
2698
2717
  skillPath: "quality-review/SKILL.md"
2699
2718
  },
2719
+ {
2720
+ name: "audit",
2721
+ description: "Run comprehensive code audit for architecture, dead code, and test quality",
2722
+ skillPath: "audit/SKILL.md"
2723
+ },
2700
2724
  {
2701
2725
  name: "refactor",
2702
2726
  description: REFACTOR_DESCRIPTION,
2703
2727
  skillPath: "refactor/SKILL.md"
2704
2728
  },
2729
+ {
2730
+ name: "retro",
2731
+ description: 'Run a safeword retrospective on the current session on demand \u2014 mine the transcript for friction (bugs / rough edges / gaps) and file it behind the egress guard. Use when the user says "run a retro", "/retro", or wants to capture friction before the session ends.',
2732
+ skillPath: "retro/SKILL.md"
2733
+ },
2705
2734
  {
2706
2735
  name: "testing",
2707
2736
  description: "How to write good tests \u2014 quality knowledge for any testing context",
@@ -2913,6 +2942,15 @@ command = 'bun "$(git rev-parse --show-toplevel)/.safeword/hooks/prompt-timestam
2913
2942
  timeout = 5
2914
2943
  statusMessage = "Adding current timestamp"
2915
2944
  `;
2945
+ var CODEX_PROMPT_RETRO_NUDGE_HOOK_PATCH = `
2946
+ [[hooks.UserPromptSubmit]]
2947
+
2948
+ [[hooks.UserPromptSubmit.hooks]]
2949
+ type = "command"
2950
+ command = 'bun "$(git rev-parse --show-toplevel)/.safeword/hooks/prompt-retro-nudge.ts"'
2951
+ timeout = 30
2952
+ statusMessage = "Checking spooled safeword retro drafts"
2953
+ `;
2916
2954
  var CODEX_SESSION_START_HOOK_PATCH = `
2917
2955
  [[hooks.SessionStart]]
2918
2956
  matcher = ""
@@ -2949,8 +2987,8 @@ var CODEX_STOP_HOOK_PATCH = `
2949
2987
  [[hooks.Stop.hooks]]
2950
2988
  type = "command"
2951
2989
  command = 'bun "$(git rev-parse --show-toplevel)/.safeword/hooks/codex/stop.ts"'
2952
- timeout = 30
2953
- statusMessage = "Checking safeword Stop nudges"
2990
+ timeout = 600
2991
+ statusMessage = "Running safeword retro if this session is substantial"
2954
2992
  `;
2955
2993
  var CODEX_POST_TOOL_SKILL_NUDGE_HOOK_PATCH = `
2956
2994
  [[hooks.PostToolUse]]
@@ -2997,6 +3035,7 @@ var CODEX_SKILL_TEMPLATE_FILES = [
2997
3035
  ["lint/SKILL.md", "skills/lint/SKILL.md"],
2998
3036
  ["quality-review/SKILL.md", "skills/quality-review/SKILL.md"],
2999
3037
  ["refactor/SKILL.md", "skills/refactor/SKILL.md"],
3038
+ ["retro/SKILL.md", "skills/retro/SKILL.md"],
3000
3039
  ["review-spec/SKILL.md", "skills/review-spec/SKILL.md"],
3001
3040
  ["self-review/SKILL.md", "skills/self-review/SKILL.md"],
3002
3041
  ["tdd-review/SKILL.md", "skills/tdd-review/SKILL.md"],
@@ -3268,6 +3307,9 @@ var SAFEWORD_SCHEMA = {
3268
3307
  ".safeword/hooks/lib/cursor-run-identity.ts": {
3269
3308
  template: "hooks/lib/cursor-run-identity.ts"
3270
3309
  },
3310
+ ".safeword/hooks/lib/cursor-state.ts": {
3311
+ template: "hooks/lib/cursor-state.ts"
3312
+ },
3271
3313
  ".safeword/hooks/lib/git-operation.ts": { template: "hooks/lib/git-operation.ts" },
3272
3314
  ".safeword/hooks/lib/re-entry.ts": { template: "hooks/lib/re-entry.ts" },
3273
3315
  ".safeword/hooks/lib/hierarchy.ts": { template: "hooks/lib/hierarchy.ts" },
@@ -3566,6 +3608,7 @@ var SAFEWORD_SCHEMA = {
3566
3608
  ".claude/skills/cleanup-zombies/SKILL.md": {
3567
3609
  template: "skills/cleanup-zombies/SKILL.md"
3568
3610
  },
3611
+ ".claude/skills/retro/SKILL.md": { template: "skills/retro/SKILL.md" },
3569
3612
  // Claude skills — contextual (auto-triggered, no slash command)
3570
3613
  ".claude/skills/brainstorm/SKILL.md": {
3571
3614
  template: "skills/brainstorm/SKILL.md"
@@ -3589,7 +3632,6 @@ var SAFEWORD_SCHEMA = {
3589
3632
  ".cursor/commands/verify.md": { template: "commands/verify.md" },
3590
3633
  ".cursor/commands/self-review.md": { template: "commands/self-review.md" },
3591
3634
  ".cursor/commands/review-spec.md": { template: "commands/review-spec.md" },
3592
- ".cursor/commands/audit.md": { template: "commands/audit.md" },
3593
3635
  ".cursor/commands/cleanup-zombies.md": {
3594
3636
  template: "commands/cleanup-zombies.md"
3595
3637
  },
@@ -3812,6 +3854,7 @@ ${managedPrettierPaths(ctx).join("\n")}
3812
3854
  ".safeword/hooks/codex/pre-tool-quality.ts"
3813
3855
  ],
3814
3856
  unpatchContent: [
3857
+ CODEX_PROMPT_RETRO_NUDGE_HOOK_PATCH,
3815
3858
  CODEX_SESSION_START_HOOK_PATCH,
3816
3859
  CODEX_LEGACY_CONTEXT_SESSION_START_HOOK_PATCH,
3817
3860
  CODEX_PRE_TOOL_QUALITY_HOOK_PATCH,
@@ -3831,6 +3874,18 @@ ${managedPrettierPaths(ctx).join("\n")}
3831
3874
  ".safeword/hooks/codex/pre-tool-quality.ts"
3832
3875
  ]
3833
3876
  },
3877
+ // Prompt retro nudge retrofit (CDX602): add-if-missing onto existing Codex
3878
+ // configs. This is Lane 2 for unfiled spooled drafts; Codex Stop itself is
3879
+ // silent and never blocks the turn.
3880
+ {
3881
+ operation: "append",
3882
+ content: CODEX_PROMPT_RETRO_NUDGE_HOOK_PATCH,
3883
+ marker: ".safeword/hooks/prompt-retro-nudge.ts",
3884
+ applyWhenContentIncludes: [
3885
+ "# Safeword Codex project configuration.",
3886
+ ".safeword/hooks/codex/pre-tool-quality.ts"
3887
+ ]
3888
+ },
3834
3889
  // Migrate existing installs (auto-upgrade-codex follow-up to #433): swap the
3835
3890
  // legacy context-only SessionStart hook for the auto-upgrade dispatcher.
3836
3891
  // Codex runs same-event hooks concurrently with no ordering, so this must
@@ -4215,4 +4270,4 @@ export {
4215
4270
  untrackIgnoredFiles,
4216
4271
  createProjectContext
4217
4272
  };
4218
- //# sourceMappingURL=chunk-GEC7AHRF.js.map
4273
+ //# sourceMappingURL=chunk-G7IU3XBI.js.map