haansi 0.1.49 → 0.1.50

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 (2) hide show
  1. package/dist/haansi.js +994 -11
  2. package/package.json +1 -1
package/dist/haansi.js CHANGED
@@ -95,7 +95,7 @@ var require_package = __commonJS({
95
95
  "package.json"(exports2, module2) {
96
96
  module2.exports = {
97
97
  name: "haansi",
98
- version: "0.1.49",
98
+ version: "0.1.50",
99
99
  description: "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
100
100
  bin: {
101
101
  haansi: "./dist/haansi.js"
@@ -17785,6 +17785,78 @@ var init_exclusions = __esm({
17785
17785
  category: "operational",
17786
17786
  reason: "Operator annotation on a ledger row (why a CONCURRENTLY migration was applied outside a transaction, that a row was backfill-stamped). Written by us about our own schema changes, not by or about a tenant."
17787
17787
  },
17788
+ // ── integrations ledger and the embedding batch queue, classified 2026-10-25 ──
17789
+ // Both reached main unclassified; the ratchet caught them once CI could build.
17790
+ {
17791
+ database: "haansi_integrations_prod",
17792
+ table: "schema_migrations",
17793
+ column: "id",
17794
+ category: "operational",
17795
+ reason: "The ledger key for one migration file (its group and name). Names our own schema change, never tenant data, and is how the ledger refuses to apply a file twice."
17796
+ },
17797
+ {
17798
+ database: "haansi_integrations_prod",
17799
+ table: "schema_migrations",
17800
+ column: "migration_path",
17801
+ category: "operational",
17802
+ reason: "The repository path of the applied migration file. Ours, not a tenant's, and SOC 2 CC8.1 evidence that must stay readable."
17803
+ },
17804
+ {
17805
+ database: "haansi_integrations_prod",
17806
+ table: "schema_migrations",
17807
+ column: "migration_group",
17808
+ category: "operational",
17809
+ reason: "The migrations/<group> directory a file belongs to: a fixed vocabulary of our own schema areas."
17810
+ },
17811
+ {
17812
+ database: "haansi_integrations_prod",
17813
+ table: "schema_migrations",
17814
+ column: "connection_kind",
17815
+ category: "operational",
17816
+ reason: "Which role applied the file, app or admin (a CHECK-constrained vocabulary). Audit evidence about our own deploys."
17817
+ },
17818
+ {
17819
+ database: "haansi_integrations_prod",
17820
+ table: "schema_migrations",
17821
+ column: "checksum_sha256",
17822
+ category: "operational",
17823
+ reason: "The file's SHA-256, compared on every run to catch a migration edited after it was applied. A hash of our own source; encrypting it would break that comparison."
17824
+ },
17825
+ {
17826
+ database: "haansi_integrations_prod",
17827
+ table: "schema_migrations",
17828
+ column: "applied_by",
17829
+ category: "operational",
17830
+ reason: "The database role that applied the migration (DEFAULT current_user), such as haansi_migrator. Names our deploy role, not a person or a tenant, and is audit evidence."
17831
+ },
17832
+ {
17833
+ database: "haansi_intelligence_prod",
17834
+ table: "embedding_queue",
17835
+ column: "target_table",
17836
+ category: "structural",
17837
+ reason: "Which intelligence table a queued row points at (knowledge_artifacts, exchange_posts). A fixed vocabulary of our own table names; the queue holds pointers only and the embed text never rests here."
17838
+ },
17839
+ {
17840
+ database: "haansi_intelligence_prod",
17841
+ table: "embedding_batch_jobs",
17842
+ column: "provider_job_id",
17843
+ category: "operational",
17844
+ reason: "The embedding provider's id for a submitted batch, used to poll and collect it. Issued for our request; no tenant content (the table has no organization_id by design)."
17845
+ },
17846
+ {
17847
+ database: "haansi_intelligence_prod",
17848
+ table: "embedding_batch_jobs",
17849
+ column: "input_file_id",
17850
+ category: "operational",
17851
+ reason: "The provider's id for the uploaded batch input file: an opaque handle. The file's content is never stored here."
17852
+ },
17853
+ {
17854
+ database: "haansi_intelligence_prod",
17855
+ table: "embedding_batch_jobs",
17856
+ column: "model",
17857
+ category: "not-personal",
17858
+ reason: "The embedding model a batch ran on, a model name we choose. Kept readable so a batch's vectors can be matched to the model that produced them."
17859
+ },
17788
17860
  {
17789
17861
  database: "haansi_integrations_prod",
17790
17862
  table: "source_accounts",
@@ -18002,6 +18074,20 @@ var init_exclusions = __esm({
18002
18074
  category: "operational",
18003
18075
  reason: "A nine-value CHECK-constrained enum (created, commented, reviewed, \u2026) copied from the canonical activity. No personal data."
18004
18076
  },
18077
+ {
18078
+ database: "haansi_intelligence_prod",
18079
+ table: "mining_initial_loads",
18080
+ column: "state",
18081
+ category: "operational",
18082
+ reason: "Closed CHECK vocabulary (holding, released) for the initial-load hold (object-graph/135). No content or personal data."
18083
+ },
18084
+ {
18085
+ database: "haansi_intelligence_prod",
18086
+ table: "mining_initial_loads",
18087
+ column: "reason",
18088
+ category: "operational",
18089
+ reason: "Closed CHECK vocabulary for why the initial-load hold is held or released (initial_sync_pending, sessions_loading, ready, \u2026). No content or personal data."
18090
+ },
18005
18091
  {
18006
18092
  database: "haansi_intelligence_prod",
18007
18093
  table: "organization_mining_config",
@@ -18880,7 +18966,414 @@ var init_exclusions = __esm({
18880
18966
  column: "event",
18881
18967
  category: "operational",
18882
18968
  reason: "Closed CHECK vocabulary (loaded, resource_loaded, synced, applied, helped, not_helped)."
18883
- }
18969
+ },
18970
+ ...exchangeExclusions(INTEL, "change_outcomes", {
18971
+ repository: [
18972
+ "not-personal",
18973
+ "An `owner/repo` name, the key the ledger groups and joins on; plaintext in objects.attributes.repository for the same reason."
18974
+ ],
18975
+ head_sha: [
18976
+ "join-key",
18977
+ "A commit hash, matched by equality against CI runs and commits."
18978
+ ],
18979
+ merge_commit_sha: [
18980
+ "join-key",
18981
+ "A commit hash, matched by equality against reverts and commits."
18982
+ ],
18983
+ head_ref: [
18984
+ "not-personal",
18985
+ "The PR's head branch name; plaintext in objects.attributes.branch_name and in coding_session_change_links.branch."
18986
+ ],
18987
+ ci_status: [
18988
+ "operational",
18989
+ "Closed CHECK vocabulary (passed, failed, pending, none)."
18990
+ ],
18991
+ main_ci_status: [
18992
+ "operational",
18993
+ "Closed CHECK vocabulary (passed, failed, pending, none)."
18994
+ ],
18995
+ reverted_by: [
18996
+ "join-key",
18997
+ "A canonical object id (github:<repo>#<n> or github:commit:<sha>), never text."
18998
+ ],
18999
+ fix_forward_by: [
19000
+ "join-key",
19001
+ "A canonical object id (github:<repo>#<n>), never text."
19002
+ ],
19003
+ survival_14d: [
19004
+ "operational",
19005
+ "Closed CHECK vocabulary (held, failed, pending)."
19006
+ ],
19007
+ survival_30d: [
19008
+ "operational",
19009
+ "Closed CHECK vocabulary (held, failed, pending)."
19010
+ ],
19011
+ agent_tool: [
19012
+ "not-personal",
19013
+ "Names a coding agent product from a fixed list in the attribution code."
19014
+ ],
19015
+ agent_model: [
19016
+ "not-personal",
19017
+ "Names a model, read from an agent's commit trailer."
19018
+ ],
19019
+ attribution_method: [
19020
+ "operational",
19021
+ "Closed CHECK vocabulary (session, trailer, pr_metadata)."
19022
+ ]
19023
+ }),
19024
+ ...exchangeExclusions(INTEL, "workstream_attempts", {
19025
+ process_id: [
19026
+ "join-key",
19027
+ "The workstream's process_registry.process_id, joined by equality."
19028
+ ],
19029
+ repository: [
19030
+ "not-personal",
19031
+ "An `owner/repo` name; plaintext in change_outcomes.repository."
19032
+ ],
19033
+ result: [
19034
+ "operational",
19035
+ "Closed CHECK vocabulary (failed, held, open, superseded)."
19036
+ ],
19037
+ failure_signals: [
19038
+ "operational",
19039
+ "Closed CHECK vocabulary (ci_failed, main_ci_failed, reverted, closed_unmerged)."
19040
+ ]
19041
+ }),
19042
+ ...exchangeExclusions(INTEL, "problem_families", {
19043
+ status: [
19044
+ "operational",
19045
+ "Closed CHECK vocabulary (active, dissolved, merged)."
19046
+ ],
19047
+ tier: ["operational", "Closed CHECK vocabulary (recurring, playbook)."],
19048
+ seed_process_id: [
19049
+ "join-key",
19050
+ "The seed workstream's process_registry.process_id, joined by equality."
19051
+ ]
19052
+ }),
19053
+ ...exchangeExclusions(INTEL, "problem_family_feedback", {
19054
+ action: [
19055
+ "operational",
19056
+ "Closed CHECK vocabulary (rename, merge, split, remove_member)."
19057
+ ],
19058
+ process_ids: [
19059
+ "join-key",
19060
+ "Workstream process ids a correction moved, joined by equality."
19061
+ ]
19062
+ }),
19063
+ ...exchangeExclusions(INTEL, "family_matches", {
19064
+ source: [
19065
+ "operational",
19066
+ "Closed CHECK vocabulary (task, ci_failure, alert)."
19067
+ ],
19068
+ source_ref: [
19069
+ "join-key",
19070
+ "owner/repo#n of a failing PR or an incident id; never task text."
19071
+ ],
19072
+ signals: [
19073
+ "operational",
19074
+ "Three numeric match signals (text, area, signature); no content."
19075
+ ],
19076
+ arm: ["operational", "Closed CHECK vocabulary (served, control)."],
19077
+ session_id: [
19078
+ "join-key",
19079
+ "The coding session's id, joined by equality to coding_session_solutions."
19080
+ ]
19081
+ }),
19082
+ ...exchangeExclusions(INTEL, "coding_session_link_rejections", {
19083
+ session_id: [
19084
+ "join-key",
19085
+ "The coding session's id, joined by equality to its change links."
19086
+ ],
19087
+ repository: [
19088
+ "not-personal",
19089
+ "An owner/repo name, matched to change links by equality; plaintext in coding_session_change_links.repository."
19090
+ ]
19091
+ }),
19092
+ ...exchangeExclusions(INTEL, "playbook_rollouts", {
19093
+ status: [
19094
+ "operational",
19095
+ "Closed CHECK vocabulary (running, promoted, rolled_back)."
19096
+ ]
19097
+ }),
19098
+ ...exchangeExclusions(INTEL, "workstream_decisions", {
19099
+ process_id: [
19100
+ "join-key",
19101
+ "The workstream's process_registry.process_id, joined by equality."
19102
+ ],
19103
+ trigger: [
19104
+ "operational",
19105
+ "Closed CHECK vocabulary (concluded, concluding_event)."
19106
+ ],
19107
+ status: [
19108
+ "operational",
19109
+ "Closed CHECK vocabulary (draft, confirmed, dismissed)."
19110
+ ],
19111
+ outcome: [
19112
+ "operational",
19113
+ "Closed CHECK vocabulary (held, reversed, revisited)."
19114
+ ],
19115
+ change_keys: [
19116
+ "join-key",
19117
+ "owner/repo#n of the workstream's PRs, as in workstream_attempts."
19118
+ ],
19119
+ model: ["operational", "The model name that drafted it."],
19120
+ prompt_tag: ["operational", "The prompt version that drafted it."]
19121
+ }),
19122
+ ...exchangeExclusions(INTEL, "workstream_decision_checks", {
19123
+ process_id: [
19124
+ "join-key",
19125
+ "The workstream's process_registry.process_id, joined by equality."
19126
+ ],
19127
+ trigger: [
19128
+ "operational",
19129
+ "Closed CHECK vocabulary (concluded, concluding_event)."
19130
+ ],
19131
+ result: ["operational", "Closed CHECK vocabulary (none, invalid)."]
19132
+ }),
19133
+ ...exchangeExclusions(INTEL, "coding_session_haansi_usage", {
19134
+ session_id: [
19135
+ "join-key",
19136
+ "The coding session's id, as in coding_session_setups, joined by equality."
19137
+ ],
19138
+ inputs: [
19139
+ "not-personal",
19140
+ "Counts of Haansi MCP calls per input type, and Haansi's own tool names."
19141
+ ]
19142
+ }),
19143
+ ...exchangeExclusions(INTEL, "readiness_snapshots", {
19144
+ baseline: [
19145
+ "not-personal",
19146
+ "Counts by month, top-level code area (repo paths) and workstream category."
19147
+ ],
19148
+ failure_signatures: [
19149
+ "not-personal",
19150
+ "Counts by Actions job and step name, from CI config."
19151
+ ]
19152
+ }),
19153
+ ...exchangeExclusions(INTEL, "replay_results", {
19154
+ process_id: [
19155
+ "join-key",
19156
+ "The replayed workstream's process_registry.process_id, joined by equality."
19157
+ ],
19158
+ arm: ["operational", "Closed CHECK vocabulary (baseline, haansi)."],
19159
+ agent: ["operational", "The agent CLI's name, e.g. claude-code."],
19160
+ model: ["operational", "The model name the replay ran with."],
19161
+ run_ref: ["not-personal", "A link to the customer's CI run."]
19162
+ }),
19163
+ ...exchangeExclusions(INTEL, "replay_tasks", {
19164
+ process_id: [
19165
+ "join-key",
19166
+ "The member workstream's process_registry.process_id, joined by equality."
19167
+ ],
19168
+ repository: [
19169
+ "join-key",
19170
+ "owner/repo of the workstream's PRs, as in workstream_attempts."
19171
+ ],
19172
+ base_sha: ["not-personal", "A commit SHA."],
19173
+ target_sha: ["not-personal", "A commit SHA."],
19174
+ checks: [
19175
+ "not-personal",
19176
+ "Actions workflow names from CI config that passed on the fix."
19177
+ ]
19178
+ }),
19179
+ ...exchangeExclusions(INTEL, "problem_family_members", {
19180
+ process_id: [
19181
+ "join-key",
19182
+ "A member workstream's process_registry.process_id, joined by equality."
19183
+ ],
19184
+ role: ["operational", "Closed CHECK vocabulary (smooth, rough, other)."],
19185
+ areas: [
19186
+ "not-personal",
19187
+ "Repository directories the member's held PRs changed; the same class as changed paths' directories shown on the Families view."
19188
+ ],
19189
+ signatures: [
19190
+ "not-personal",
19191
+ 'CI job and step names ("job \u203A step"), normalized; no log text.'
19192
+ ]
19193
+ }),
19194
+ ...exchangeExclusions(INTEL, "workstream_root_causes", {
19195
+ process_id: [
19196
+ "join-key",
19197
+ "The workstream's process_registry.process_id, joined by equality."
19198
+ ],
19199
+ confidence: ["operational", "Closed CHECK vocabulary (high, medium, low)."],
19200
+ input_hash: ["not-personal", "A SHA-256 of the inputs; no content."],
19201
+ input_keys: [
19202
+ "not-personal",
19203
+ "owner/repo#n keys of the PRs the statement was written from."
19204
+ ],
19205
+ model: ["not-personal", "The model name that wrote the statement."],
19206
+ prompt_tag: ["operational", "The prompt version tag."]
19207
+ }),
19208
+ ...exchangeExclusions(INTEL, "problem_family_findings", {
19209
+ findings: [
19210
+ "operational",
19211
+ "Setup factor labels (action steps, skill names, agent, model, effort, instruction-file path and hash prefix) and pair counts, copied from coding_session_setups, which is itself plaintext; read only by viewers who can see every author in author_ids."
19212
+ ],
19213
+ author_ids: [
19214
+ "join-key",
19215
+ "organization_users ids whose sessions the findings drew on, compared with the viewer's visible authors."
19216
+ ]
19217
+ }),
19218
+ ...exchangeExclusions(INTEL, "pull_request_checks", {
19219
+ repository: [
19220
+ "join-key",
19221
+ "owner/repo of the PR, joined by equality to workstream_attempts.repository."
19222
+ ],
19223
+ checks: [
19224
+ "not-personal",
19225
+ "Actions workflow names from the repository's CI config, with closed status/conclusion values and run counts; no logs or output."
19226
+ ],
19227
+ failed: [
19228
+ "not-personal",
19229
+ 'Normalized "job \u203A step" names from CI config, the same signatures problem families compare; no logs or output.'
19230
+ ]
19231
+ }),
19232
+ ...exchangeExclusions(INTEL, "attempt_rule_configs", {
19233
+ config: [
19234
+ "operational",
19235
+ "Numeric rule settings per workstream category; no content."
19236
+ ],
19237
+ created_by: [
19238
+ "operational",
19239
+ "Closed CHECK vocabulary (tuning, platform_admin, defaults)."
19240
+ ],
19241
+ reason: [
19242
+ "operational",
19243
+ "Generated text naming the settings a version changed and the label counts behind it."
19244
+ ]
19245
+ }),
19246
+ ...exchangeExclusions(INTEL, "attempt_rule_evaluations", {
19247
+ config: [
19248
+ "operational",
19249
+ "The numeric rule settings the run scored; no content."
19250
+ ],
19251
+ metrics: ["operational", "Per-category label and hit counts; no content."],
19252
+ decision: [
19253
+ "operational",
19254
+ "Closed CHECK vocabulary (kept, stepped, too_few_labels)."
19255
+ ],
19256
+ changes: ["operational", "The settings a run stepped, as numbers."]
19257
+ }),
19258
+ ...exchangeExclusions(INTEL, "coding_session_solutions", {
19259
+ segment_branch: [
19260
+ "not-personal",
19261
+ "A git branch name, matched to commit links' branch by equality; plaintext like coding_session_change_links.branch."
19262
+ ],
19263
+ outcome: [
19264
+ "operational",
19265
+ "Closed CHECK vocabulary (held, failed) stamped by the outcome ledger from the solution's linked PRs."
19266
+ ]
19267
+ }),
19268
+ ...exchangeExclusions(INTEL, "coding_session_setups", {
19269
+ repository: [
19270
+ "join-key",
19271
+ "owner/repo from the session's git remote, joined to PRs by equality."
19272
+ ],
19273
+ branch: [
19274
+ "join-key",
19275
+ "The git branch the session uploaded on, joined to a PR's head branch; plaintext like coding_session_change_links.branch."
19276
+ ],
19277
+ session_id: [
19278
+ "join-key",
19279
+ "The coding session's id, joined by equality to coding_session_solutions."
19280
+ ],
19281
+ agent_tool: [
19282
+ "operational",
19283
+ "Names a coding agent product (claude-code, codex) from the parser."
19284
+ ],
19285
+ models: ["not-personal", "Model names the agent's transcript recorded."],
19286
+ effort: ["operational", "A reasoning-effort level such as high or medium."],
19287
+ git_commit: [
19288
+ "not-personal",
19289
+ "A commit SHA; plaintext in change_outcomes and coding_session_change_links."
19290
+ ],
19291
+ instructions: [
19292
+ "not-personal",
19293
+ "Instruction-file paths with the home directory stripped, a closed kind and a content hash; no file content."
19294
+ ],
19295
+ skills: [
19296
+ "not-personal",
19297
+ "Skill names the agent loaded and Haansi skill/revision ids; no skill content."
19298
+ ],
19299
+ context: [
19300
+ "not-personal",
19301
+ "Counts of reads, searches, web fetches and subagents, and MCP tool names; no paths, queries or content."
19302
+ ],
19303
+ prompt_turn_uuid: [
19304
+ "join-key",
19305
+ "A transcript message uuid: the prompt by reference, never its text."
19306
+ ],
19307
+ prompt_fingerprint: [
19308
+ "not-personal",
19309
+ "A 64-bit SimHash of the prompt's words; the text cannot be recovered from it."
19310
+ ],
19311
+ actions: ["operational", "Closed CHECK vocabulary of action step types."]
19312
+ }),
19313
+ ...exchangeExclusions(INTEL, "workstream_attempt_labels", {
19314
+ process_id: [
19315
+ "join-key",
19316
+ "The workstream's process_registry.process_id, joined by equality."
19317
+ ],
19318
+ repository: [
19319
+ "not-personal",
19320
+ "An `owner/repo` name; plaintext in workstream_attempts.repository."
19321
+ ],
19322
+ label: [
19323
+ "operational",
19324
+ "Closed CHECK vocabulary (right_fix, wrong_fix, fixed_by, not_failure, not_replaced, cleared)."
19325
+ ],
19326
+ rule_result: [
19327
+ "operational",
19328
+ "The rule's result when labeled: workstream_attempts.result vocabulary."
19329
+ ]
19330
+ }),
19331
+ ...exchangeExclusions(INTEL, "coding_session_lineage", {
19332
+ session_id: [
19333
+ "join-key",
19334
+ "The coding session's id, joined by equality to coding_session_solutions."
19335
+ ],
19336
+ root_turn_uuid: [
19337
+ "join-key",
19338
+ "A transcript message uuid, matched by equality to find resumed sessions."
19339
+ ],
19340
+ last_turn_uuid: [
19341
+ "join-key",
19342
+ "A transcript message uuid, looked up in a resumed session's messages."
19343
+ ],
19344
+ replaced_by_session_id: [
19345
+ "join-key",
19346
+ "The replacing coding session's id, joined by equality when superseding."
19347
+ ]
19348
+ }),
19349
+ ...exchangeExclusions(INTEL, "coding_session_change_scans", {
19350
+ session_id: [
19351
+ "join-key",
19352
+ "The coding session's id, joined by equality to coding_session_solutions."
19353
+ ]
19354
+ }),
19355
+ ...exchangeExclusions(INTEL, "coding_session_change_links", {
19356
+ session_id: [
19357
+ "join-key",
19358
+ "The coding session's id, joined by equality to coding_session_solutions."
19359
+ ],
19360
+ kind: [
19361
+ "operational",
19362
+ "Closed CHECK vocabulary (commit, pr_created, pr_merged)."
19363
+ ],
19364
+ commit_sha: [
19365
+ "join-key",
19366
+ "A commit hash, matched by prefix against PR commits."
19367
+ ],
19368
+ branch: [
19369
+ "not-personal",
19370
+ "A git branch name, matched against PR head branches; plaintext in objects.attributes.branch_name for the same reason."
19371
+ ],
19372
+ repository: [
19373
+ "not-personal",
19374
+ "An `owner/repo` name, the key links are resolved within; plaintext in change_outcomes.repository."
19375
+ ]
19376
+ })
18884
19377
  ];
18885
19378
  }
18886
19379
  });
@@ -25868,7 +26361,132 @@ var init_table_registry = __esm({
25868
26361
  "artifact_cluster_workstreams",
25869
26362
  "direct",
25870
26363
  { scopingColumn: "organization_id" }
25871
- )
26364
+ ),
26365
+ // Outcome ledger v0: derived per merged PR from the org's source_events by
26366
+ // the service-pipeline outcome-ledger tick, on the org's userdata pool.
26367
+ entry("intelligence", INTELLIGENCE_DB, "change_outcomes", "direct", {
26368
+ scopingColumn: "organization_id"
26369
+ }),
26370
+ // Attempt chains: each mined workstream's PRs, failures and fix-forwards.
26371
+ entry("intelligence", INTELLIGENCE_DB, "workstream_attempts", "direct", {
26372
+ scopingColumn: "organization_id"
26373
+ }),
26374
+ // Problem families: resolved workstreams grouped by problem, and runs.
26375
+ entry("intelligence", INTELLIGENCE_DB, "problem_families", "direct", {
26376
+ scopingColumn: "organization_id"
26377
+ }),
26378
+ entry("intelligence", INTELLIGENCE_DB, "problem_family_members", "direct", {
26379
+ scopingColumn: "organization_id"
26380
+ }),
26381
+ entry("intelligence", INTELLIGENCE_DB, "problem_family_runs", "direct", {
26382
+ scopingColumn: "organization_id"
26383
+ }),
26384
+ // Matches of new work to a family (playbooks plan §4.8).
26385
+ entry("intelligence", INTELLIGENCE_DB, "family_matches", "direct", {
26386
+ scopingColumn: "organization_id"
26387
+ }),
26388
+ // Window links a session's author rejected (sessions plan §3.1).
26389
+ entry(
26390
+ "intelligence",
26391
+ INTELLIGENCE_DB,
26392
+ "coding_session_link_rejections",
26393
+ "direct",
26394
+ {
26395
+ scopingColumn: "organization_id"
26396
+ }
26397
+ ),
26398
+ // Rollouts of a family's playbook revision (playbooks plan phase 6).
26399
+ entry("intelligence", INTELLIGENCE_DB, "playbook_rollouts", "direct", {
26400
+ scopingColumn: "organization_id"
26401
+ }),
26402
+ // Decisions drafted when a workstream concludes (blueprint, Decisions).
26403
+ entry("intelligence", INTELLIGENCE_DB, "workstream_decisions", "direct", {
26404
+ scopingColumn: "organization_id"
26405
+ }),
26406
+ // Workstreams the ledger found to have no decision (never shown).
26407
+ entry(
26408
+ "intelligence",
26409
+ INTELLIGENCE_DB,
26410
+ "workstream_decision_checks",
26411
+ "direct",
26412
+ {
26413
+ scopingColumn: "organization_id"
26414
+ }
26415
+ ),
26416
+ // What Haansi gave each coding session (readiness restructure plan §4.6).
26417
+ entry(
26418
+ "intelligence",
26419
+ INTELLIGENCE_DB,
26420
+ "coding_session_haansi_usage",
26421
+ "direct",
26422
+ {
26423
+ scopingColumn: "organization_id"
26424
+ }
26425
+ ),
26426
+ // The readiness report's baseline (readiness restructure plan §4.1).
26427
+ entry("intelligence", INTELLIGENCE_DB, "readiness_snapshots", "direct", {
26428
+ scopingColumn: "organization_id"
26429
+ }),
26430
+ // Replay results reported from the customer's CI (readiness plan §4.6).
26431
+ entry("intelligence", INTELLIGENCE_DB, "replay_results", "direct", {
26432
+ scopingColumn: "organization_id"
26433
+ }),
26434
+ // Replay tasks cut from family members (playbooks plan §4.7).
26435
+ entry("intelligence", INTELLIGENCE_DB, "replay_tasks", "direct", {
26436
+ scopingColumn: "organization_id"
26437
+ }),
26438
+ entry("intelligence", INTELLIGENCE_DB, "problem_family_feedback", "direct", {
26439
+ scopingColumn: "organization_id"
26440
+ }),
26441
+ entry("intelligence", INTELLIGENCE_DB, "workstream_root_causes", "direct", {
26442
+ scopingColumn: "organization_id"
26443
+ }),
26444
+ entry("intelligence", INTELLIGENCE_DB, "problem_family_findings", "direct", {
26445
+ scopingColumn: "organization_id"
26446
+ }),
26447
+ // The checks that ran on each PR's head commit.
26448
+ entry("intelligence", INTELLIGENCE_DB, "pull_request_checks", "direct", {
26449
+ scopingColumn: "organization_id"
26450
+ }),
26451
+ // The attempt rule's per-org settings (versioned) and its scoring runs.
26452
+ entry("intelligence", INTELLIGENCE_DB, "attempt_rule_configs", "direct", {
26453
+ scopingColumn: "organization_id"
26454
+ }),
26455
+ entry("intelligence", INTELLIGENCE_DB, "attempt_rule_evaluations", "direct", {
26456
+ scopingColumn: "organization_id"
26457
+ }),
26458
+ // What each coding session ran with (agent, instructions, skills, actions).
26459
+ entry("intelligence", INTELLIGENCE_DB, "coding_session_setups", "direct", {
26460
+ scopingColumn: "organization_id"
26461
+ }),
26462
+ // People's labels on attempt chains; override the rule, kept for evaluation.
26463
+ entry(
26464
+ "intelligence",
26465
+ INTELLIGENCE_DB,
26466
+ "workstream_attempt_labels",
26467
+ "direct",
26468
+ { scopingColumn: "organization_id" }
26469
+ ),
26470
+ // Commits and PRs named in a coding session's transcript, recorded at mining
26471
+ // time beside the session's solutions.
26472
+ entry(
26473
+ "intelligence",
26474
+ INTELLIGENCE_DB,
26475
+ "coding_session_change_links",
26476
+ "direct",
26477
+ { scopingColumn: "organization_id" }
26478
+ ),
26479
+ entry(
26480
+ "intelligence",
26481
+ INTELLIGENCE_DB,
26482
+ "coding_session_change_scans",
26483
+ "direct",
26484
+ { scopingColumn: "organization_id" }
26485
+ ),
26486
+ // Resumed-session lineage: which session file replaced which.
26487
+ entry("intelligence", INTELLIGENCE_DB, "coding_session_lineage", "direct", {
26488
+ scopingColumn: "organization_id"
26489
+ })
25872
26490
  ];
25873
26491
  INTEGRATIONS_DB = "haansi_integrations_prod";
25874
26492
  integrationTables = [
@@ -76696,10 +77314,10 @@ var require_view = __commonJS({
76696
77314
  var debug = require_src()("express:view");
76697
77315
  var path2 = require("node:path");
76698
77316
  var fs = require("node:fs");
76699
- var dirname4 = path2.dirname;
77317
+ var dirname5 = path2.dirname;
76700
77318
  var basename4 = path2.basename;
76701
77319
  var extname = path2.extname;
76702
- var join15 = path2.join;
77320
+ var join16 = path2.join;
76703
77321
  var resolve5 = path2.resolve;
76704
77322
  module2.exports = View;
76705
77323
  function View(name, options) {
@@ -76735,7 +77353,7 @@ var require_view = __commonJS({
76735
77353
  for (var i = 0; i < roots.length && !path3; i++) {
76736
77354
  var root = roots[i];
76737
77355
  var loc = resolve5(root, name);
76738
- var dir = dirname4(loc);
77356
+ var dir = dirname5(loc);
76739
77357
  var file2 = basename4(loc);
76740
77358
  path3 = this.resolve(dir, file2);
76741
77359
  }
@@ -76761,12 +77379,12 @@ var require_view = __commonJS({
76761
77379
  };
76762
77380
  View.prototype.resolve = function resolve6(dir, file2) {
76763
77381
  var ext = this.ext;
76764
- var path3 = join15(dir, file2);
77382
+ var path3 = join16(dir, file2);
76765
77383
  var stat = tryStat(path3);
76766
77384
  if (stat && stat.isFile()) {
76767
77385
  return path3;
76768
77386
  }
76769
- path3 = join15(dir, basename4(file2, ext), "index" + ext);
77387
+ path3 = join16(dir, basename4(file2, ext), "index" + ext);
76770
77388
  stat = tryStat(path3);
76771
77389
  if (stat && stat.isFile()) {
76772
77390
  return path3;
@@ -80411,7 +81029,7 @@ var require_send = __commonJS({
80411
81029
  var Stream = require("stream");
80412
81030
  var util2 = require("util");
80413
81031
  var extname = path2.extname;
80414
- var join15 = path2.join;
81032
+ var join16 = path2.join;
80415
81033
  var normalize = path2.normalize;
80416
81034
  var resolve5 = path2.resolve;
80417
81035
  var sep2 = path2.sep;
@@ -80583,7 +81201,7 @@ var require_send = __commonJS({
80583
81201
  return res;
80584
81202
  }
80585
81203
  parts = path3.split(sep2);
80586
- path3 = normalize(join15(root, path3));
81204
+ path3 = normalize(join16(root, path3));
80587
81205
  } else {
80588
81206
  if (UP_PATH_REGEXP.test(path3)) {
80589
81207
  debug('malicious path "%s"', path3);
@@ -80716,7 +81334,7 @@ var require_send = __commonJS({
80716
81334
  if (err2) return self.onStatError(err2);
80717
81335
  return self.error(404);
80718
81336
  }
80719
- var p = join15(path3, self._index[i]);
81337
+ var p = join16(path3, self._index[i]);
80720
81338
  debug('stat "%s"', p);
80721
81339
  fs.stat(p, function(err3, stat) {
80722
81340
  if (err3) return next(err3);
@@ -84427,6 +85045,363 @@ var init_skills = __esm({
84427
85045
  }
84428
85046
  });
84429
85047
 
85048
+ // src/commands/replay.ts
85049
+ var replay_exports = {};
85050
+ __export(replay_exports, {
85051
+ githubRunRef: () => githubRunRef,
85052
+ parseReplayArgs: () => parseReplayArgs,
85053
+ replay: () => replay,
85054
+ replayArm: () => replayArm,
85055
+ replayPrompt: () => replayPrompt,
85056
+ repositoryFromRemote: () => repositoryFromRemote,
85057
+ runCommand: () => runCommand,
85058
+ runReplays: () => runReplays,
85059
+ safeTestFile: () => safeTestFile,
85060
+ skipReason: () => skipReason
85061
+ });
85062
+ function parseReplayArgs(argv) {
85063
+ const options = {
85064
+ familyId: "",
85065
+ agentCmd: "",
85066
+ testCmd: "",
85067
+ attempts: 3,
85068
+ arms: [...ARMS],
85069
+ limit: null,
85070
+ cutoff: null,
85071
+ timeoutMs: 30 * 6e4,
85072
+ agentName: null,
85073
+ model: null,
85074
+ runRef: githubRunRef(process.env),
85075
+ dryRun: false
85076
+ };
85077
+ let sub;
85078
+ const errors = [];
85079
+ for (let i = 0; i < argv.length; i++) {
85080
+ const a = argv[i];
85081
+ const next = () => argv[++i] ?? "";
85082
+ if (a === "--family") options.familyId = next();
85083
+ else if (a === "--agent-cmd") options.agentCmd = next();
85084
+ else if (a === "--test-cmd") options.testCmd = next();
85085
+ else if (a === "--attempts") options.attempts = Number(next());
85086
+ else if (a === "--limit") options.limit = Number(next());
85087
+ else if (a === "--cutoff") options.cutoff = next();
85088
+ else if (a === "--timeout-min") options.timeoutMs = Number(next()) * 6e4;
85089
+ else if (a === "--agent-name") options.agentName = next();
85090
+ else if (a === "--model") options.model = next();
85091
+ else if (a === "--run-ref") options.runRef = next();
85092
+ else if (a === "--dry-run") options.dryRun = true;
85093
+ else if (a === "--arms")
85094
+ options.arms = next().split(",").map((s) => s.trim()).filter(Boolean);
85095
+ else if (!sub && !a.startsWith("-")) sub = a;
85096
+ else errors.push(`Unknown option: ${a}`);
85097
+ }
85098
+ if (!/^[0-9a-f-]{36}$/i.test(options.familyId))
85099
+ errors.push("--family <id> is required (the family's id).");
85100
+ if (!options.agentCmd) errors.push("--agent-cmd is required.");
85101
+ if (!options.testCmd) errors.push("--test-cmd is required.");
85102
+ if (!Number.isInteger(options.attempts) || options.attempts < 1 || options.attempts > 20)
85103
+ errors.push("--attempts must be 1 to 20.");
85104
+ if (options.limit !== null && (!Number.isInteger(options.limit) || options.limit < 1))
85105
+ errors.push("--limit must be a positive whole number.");
85106
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
85107
+ errors.push("--timeout-min must be a positive number.");
85108
+ if (options.arms.length === 0 || options.arms.some((a) => !ARMS.includes(a)))
85109
+ errors.push("--arms must list baseline and/or haansi.");
85110
+ return { sub, options, errors };
85111
+ }
85112
+ function githubRunRef(env) {
85113
+ const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } = env;
85114
+ if (!GITHUB_SERVER_URL?.startsWith("https://") || !GITHUB_REPOSITORY || !GITHUB_RUN_ID)
85115
+ return null;
85116
+ return `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
85117
+ }
85118
+ function repositoryFromRemote(remote) {
85119
+ const m = /[:/]([^/:\s]+\/[^/\s]+?)(?:\.git)?\/?\s*$/.exec(remote.trim());
85120
+ return m ? m[1].toLowerCase() : null;
85121
+ }
85122
+ function safeTestFile(path2) {
85123
+ return path2.length > 0 && !path2.startsWith("/") && !path2.startsWith("-") && !path2.split("/").includes("..") && !/[\u0000-\u001f]/.test(path2);
85124
+ }
85125
+ function skipReason(task, repository) {
85126
+ if (!repository || task.repository.toLowerCase() !== repository)
85127
+ return `in ${task.repository}, not this repository`;
85128
+ if (!task.baseSha || !SHA.test(task.baseSha)) return "no starting commit";
85129
+ if (!task.targetSha || !SHA.test(task.targetSha)) return "no fix commit";
85130
+ if (task.testFiles.length === 0) return "the fix changed no test file";
85131
+ if (!task.testFiles.every(safeTestFile)) return "a test path is not usable";
85132
+ return null;
85133
+ }
85134
+ function replayPrompt(task, arm, feedback) {
85135
+ const context = task.context.length ? task.context.map((c) => `- ${c}`).join("\n") : "- (No record of the task before the first attempt was captured.)";
85136
+ return [
85137
+ `# ${task.name ?? "A task in this repository"}`,
85138
+ "",
85139
+ "Solve the problem these describe, in this repository as it is now:",
85140
+ "",
85141
+ context,
85142
+ "",
85143
+ ...arm === "haansi" ? [
85144
+ "Your team's playbook for this kind of problem is in .haansi-replay/playbook.md. Read it first.",
85145
+ ""
85146
+ ] : [],
85147
+ ...feedback ? [
85148
+ "Your previous attempt did not pass the tests. Their output ends with:",
85149
+ "",
85150
+ "```",
85151
+ feedback,
85152
+ "```",
85153
+ ""
85154
+ ] : [],
85155
+ "Do not look at later commits or other branches."
85156
+ ].join("\n");
85157
+ }
85158
+ async function ensureCommit(deps, sha) {
85159
+ if ((await deps.git(["cat-file", "-e", `${sha}^{commit}`], deps.repoDir)).code === 0)
85160
+ return true;
85161
+ return (await deps.git(["fetch", "--quiet", "origin", sha], deps.repoDir)).code === 0;
85162
+ }
85163
+ async function replayArm(deps, options, task, arm, playbook) {
85164
+ const started = deps.now();
85165
+ const dir = (0, import_node_path9.join)(
85166
+ deps.workDir(),
85167
+ `${task.processId.replace(/[^a-zA-Z0-9_-]/g, "")}-${arm}`
85168
+ );
85169
+ const files = task.testFiles;
85170
+ const add = await deps.git(
85171
+ ["worktree", "add", "--detach", dir, task.baseSha],
85172
+ deps.repoDir
85173
+ );
85174
+ if (add.code !== 0) throw new Error(`could not check out ${task.baseSha}`);
85175
+ try {
85176
+ const promptFile = (0, import_node_path9.join)(dir, ".haansi-replay", "prompt.md");
85177
+ if (arm === "haansi" && playbook)
85178
+ deps.writeFile(
85179
+ (0, import_node_path9.join)(dir, ".haansi-replay", "playbook.md"),
85180
+ playbook.skillMd
85181
+ );
85182
+ let feedback = null;
85183
+ for (let attempt = 1; attempt <= options.attempts; attempt++) {
85184
+ const prompt2 = replayPrompt(task, arm, feedback);
85185
+ deps.writeFile(promptFile, prompt2);
85186
+ await deps.shell(
85187
+ options.agentCmd,
85188
+ dir,
85189
+ { HAANSI_REPLAY_PROMPT: prompt2, HAANSI_REPLAY_PROMPT_FILE: promptFile },
85190
+ options.timeoutMs
85191
+ );
85192
+ const own = new Map(files.map((f) => [f, deps.readFile((0, import_node_path9.join)(dir, f))]));
85193
+ const checkout = await deps.git(
85194
+ ["checkout", task.targetSha, "--", ...files],
85195
+ dir
85196
+ );
85197
+ if (checkout.code !== 0)
85198
+ throw new Error("could not check out the fix's tests");
85199
+ const test = await deps.shell(
85200
+ options.testCmd,
85201
+ dir,
85202
+ { HAANSI_REPLAY_TEST_FILES: files.join(" ") },
85203
+ options.timeoutMs
85204
+ );
85205
+ if (test.code === 0) {
85206
+ return {
85207
+ processId: task.processId,
85208
+ arm,
85209
+ passed: true,
85210
+ attempts: attempt,
85211
+ maxAttempts: options.attempts,
85212
+ durationMs: deps.now() - started,
85213
+ revisionId: arm === "haansi" ? playbook?.revisionId ?? null : null
85214
+ };
85215
+ }
85216
+ for (const [file2, content] of own) {
85217
+ if (content === null) deps.removeFile((0, import_node_path9.join)(dir, file2));
85218
+ else deps.writeFile((0, import_node_path9.join)(dir, file2), content);
85219
+ }
85220
+ feedback = test.output.slice(-FEEDBACK_CHARS);
85221
+ }
85222
+ return {
85223
+ processId: task.processId,
85224
+ arm,
85225
+ passed: false,
85226
+ attempts: options.attempts,
85227
+ maxAttempts: options.attempts,
85228
+ durationMs: deps.now() - started,
85229
+ revisionId: arm === "haansi" ? playbook?.revisionId ?? null : null
85230
+ };
85231
+ } finally {
85232
+ await deps.git(["worktree", "remove", "--force", dir], deps.repoDir);
85233
+ deps.removeDir(dir);
85234
+ }
85235
+ }
85236
+ async function runReplays(options, deps) {
85237
+ const remote = await deps.git(["remote", "get-url", "origin"], deps.repoDir);
85238
+ const repository = remote.code === 0 ? repositoryFromRemote(remote.output) : null;
85239
+ const tasks = await deps.getTasks(options.familyId, options.cutoff);
85240
+ let arms = options.arms;
85241
+ let playbook = null;
85242
+ if (arms.includes("haansi")) {
85243
+ playbook = await deps.getPlaybook(options.familyId);
85244
+ if (!playbook) {
85245
+ deps.log(
85246
+ "The family has no published playbook you can read: running the baseline arm only."
85247
+ );
85248
+ arms = arms.filter((a) => a !== "haansi");
85249
+ }
85250
+ }
85251
+ const runnable = [];
85252
+ let skipped = 0;
85253
+ for (const task of tasks) {
85254
+ const reason = skipReason(task, repository);
85255
+ if (reason) {
85256
+ skipped += 1;
85257
+ deps.log(`skip ${task.name ?? task.processId}: ${reason}`);
85258
+ } else runnable.push(task);
85259
+ }
85260
+ const chosen = options.limit ? runnable.slice(0, options.limit) : runnable;
85261
+ const results = [];
85262
+ for (const task of chosen) {
85263
+ if (!await ensureCommit(deps, task.baseSha) || !await ensureCommit(deps, task.targetSha)) {
85264
+ skipped += 1;
85265
+ deps.log(
85266
+ `skip ${task.name ?? task.processId}: its commits are not reachable from origin`
85267
+ );
85268
+ continue;
85269
+ }
85270
+ for (const arm of arms) {
85271
+ try {
85272
+ const result = await replayArm(deps, options, task, arm, playbook);
85273
+ results.push(result);
85274
+ deps.log(
85275
+ `${result.passed ? "pass" : "fail"} ${arm.padEnd(8)} ${task.name ?? task.processId} (${result.attempts}/${result.maxAttempts} attempts)`
85276
+ );
85277
+ } catch (err2) {
85278
+ deps.log(
85279
+ `error ${arm} ${task.name ?? task.processId}: ${err2 instanceof Error ? err2.message : String(err2)}`
85280
+ );
85281
+ }
85282
+ }
85283
+ }
85284
+ if (options.dryRun || results.length === 0) {
85285
+ return { results, skipped, recorded: null };
85286
+ }
85287
+ const reported = await deps.report(options.familyId, {
85288
+ agent: options.agentName,
85289
+ model: options.model,
85290
+ runRef: options.runRef,
85291
+ results
85292
+ });
85293
+ if (reported.refused.length)
85294
+ deps.log(
85295
+ `Haansi did not record ${reported.refused.length} task(s) it no longer lists for this family.`
85296
+ );
85297
+ return { results, skipped, recorded: reported.recorded };
85298
+ }
85299
+ function runCommand(file2, args, cwd, env, timeoutMs) {
85300
+ return new Promise((resolvePromise) => {
85301
+ const child = (0, import_node_child_process4.spawn)(file2, args, {
85302
+ cwd,
85303
+ env: { ...process.env, ...env },
85304
+ stdio: ["ignore", "pipe", "pipe"]
85305
+ });
85306
+ let output = "";
85307
+ const keep = (chunk) => {
85308
+ output = (output + chunk.toString()).slice(-2e5);
85309
+ };
85310
+ child.stdout.on("data", keep);
85311
+ child.stderr.on("data", keep);
85312
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
85313
+ child.on("close", (code) => {
85314
+ clearTimeout(timer);
85315
+ resolvePromise({ code: code ?? 1, output });
85316
+ });
85317
+ child.on("error", () => {
85318
+ clearTimeout(timer);
85319
+ resolvePromise({ code: 1, output });
85320
+ });
85321
+ });
85322
+ }
85323
+ async function replay(argv) {
85324
+ const { sub, options, errors } = parseReplayArgs(argv);
85325
+ if (sub !== "run" || errors.length) {
85326
+ for (const e of sub === "run" ? errors : []) console.error(e);
85327
+ console.log(USAGE);
85328
+ if (sub === "run") process.exitCode = 1;
85329
+ return;
85330
+ }
85331
+ const token = resolveToken5();
85332
+ const apiUrl2 = apiUrl();
85333
+ const work = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os9.tmpdir)(), "haansi-replay-"));
85334
+ try {
85335
+ const { results, skipped, recorded } = await runReplays(options, {
85336
+ getTasks: async (familyId, cutoff) => (await apiGet3(
85337
+ `/memory/processes/families/${familyId}/replay-tasks${cutoff ? `?cutoff=${encodeURIComponent(cutoff)}` : ""}`,
85338
+ token,
85339
+ apiUrl2
85340
+ )).replay,
85341
+ getPlaybook: async (familyId) => (await apiGet3(
85342
+ `/memory/skills/families/${familyId}/playbook`,
85343
+ token,
85344
+ apiUrl2
85345
+ ).catch(() => ({ data: { playbook: null } }))).data.playbook,
85346
+ report: async (familyId, body) => await apiPost2(
85347
+ `/memory/processes/families/${familyId}/replay-results`,
85348
+ body,
85349
+ token,
85350
+ apiUrl2
85351
+ ),
85352
+ git: (args, cwd) => runCommand("git", args, cwd, {}, 10 * 6e4),
85353
+ shell: (command2, cwd, env, timeoutMs) => runCommand("sh", ["-c", command2], cwd, env, timeoutMs),
85354
+ repoDir: process.cwd(),
85355
+ workDir: () => work,
85356
+ writeFile: (path2, content) => {
85357
+ (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(path2), { recursive: true });
85358
+ (0, import_node_fs10.writeFileSync)(path2, content);
85359
+ },
85360
+ readFile: (path2) => (0, import_node_fs10.existsSync)(path2) ? (0, import_node_fs10.readFileSync)(path2, "utf-8") : null,
85361
+ removeFile: (path2) => (0, import_node_fs10.rmSync)(path2, { force: true }),
85362
+ removeDir: (path2) => (0, import_node_fs10.rmSync)(path2, { recursive: true, force: true }),
85363
+ now: () => Date.now(),
85364
+ log: (line) => console.log(line)
85365
+ });
85366
+ const passed = (arm) => results.filter((r) => r.arm === arm && r.passed).length;
85367
+ const ran = (arm) => results.filter((r) => r.arm === arm).length;
85368
+ console.log(
85369
+ `
85370
+ Baseline: ${passed("baseline")}/${ran("baseline")} passed \xB7 With Haansi: ${passed("haansi")}/${ran("haansi")} passed \xB7 ${skipped} skipped`
85371
+ );
85372
+ console.log(
85373
+ recorded === null ? options.dryRun ? "Dry run: nothing reported." : "Nothing to report." : `Reported ${recorded} result(s) to Haansi.`
85374
+ );
85375
+ } finally {
85376
+ (0, import_node_fs10.rmSync)(work, { recursive: true, force: true });
85377
+ }
85378
+ }
85379
+ var import_node_child_process4, import_node_fs10, import_node_os9, import_node_path9, SHA, ARMS, FEEDBACK_CHARS, USAGE;
85380
+ var init_replay = __esm({
85381
+ "src/commands/replay.ts"() {
85382
+ "use strict";
85383
+ import_node_child_process4 = require("node:child_process");
85384
+ import_node_fs10 = require("node:fs");
85385
+ import_node_os9 = require("node:os");
85386
+ import_node_path9 = require("node:path");
85387
+ init_paths();
85388
+ init_api2();
85389
+ SHA = /^[0-9a-f]{7,40}$/i;
85390
+ ARMS = ["baseline", "haansi"];
85391
+ FEEDBACK_CHARS = 6e3;
85392
+ USAGE = `Usage: haansi replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85393
+ [--attempts 3] [--arms baseline,haansi] [--limit n]
85394
+ [--cutoff <date>] [--timeout-min 30]
85395
+ [--agent-name <name>] [--model <name>] [--run-ref <url>] [--dry-run]
85396
+
85397
+ Run from a clone of the repository. Example (GitHub Actions):
85398
+ haansi replay run --family "$FAMILY_ID" \\
85399
+ --agent-cmd 'claude -p "$HAANSI_REPLAY_PROMPT" --permission-mode acceptEdits' \\
85400
+ --test-cmd 'npx vitest run $HAANSI_REPLAY_TEST_FILES' \\
85401
+ --agent-name claude-code --model claude-sonnet-5-5`;
85402
+ }
85403
+ });
85404
+
84430
85405
  // src/index.ts
84431
85406
  init_paths();
84432
85407
  var { version: VERSION } = require_package();
@@ -84599,6 +85574,11 @@ Active selection (${selection.source}): ${selection.agents.join(", ")}`
84599
85574
  await skills2(process.argv.slice(3));
84600
85575
  break;
84601
85576
  }
85577
+ case "replay": {
85578
+ const { replay: replay2 } = await Promise.resolve().then(() => (init_replay(), replay_exports));
85579
+ await replay2(process.argv.slice(3));
85580
+ break;
85581
+ }
84602
85582
  case "digest": {
84603
85583
  const { digest: digest2, parseDigestRange: parseDigestRange2 } = await Promise.resolve().then(() => (init_digest(), digest_exports));
84604
85584
  await digest2(parseDigestRange2(process.argv.slice(3)));
@@ -84636,6 +85616,9 @@ Commands:
84636
85616
  digest. [--date YYYY-MM-DD] [--workstream <id>]
84637
85617
  digest [--week|--month|--since YYYY-MM-DD]
84638
85618
  Your weekly digest (default: the latest week)
85619
+ replay run --family <id> --agent-cmd "<cmd>" --test-cmd "<cmd>"
85620
+ Replay a problem family's tasks in your CI, with and
85621
+ without its playbook, and report the results
84639
85622
  skills sync [--agent <sel>] [--project] [--dir <path>]
84640
85623
  Copy your team's published skills into Claude Code / Codex
84641
85624
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haansi",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "Haansi CLI - Session collector and MCP server for Claude Code & OpenAI Codex",
5
5
  "bin": {
6
6
  "haansi": "./dist/haansi.js"