theslopmachine 0.7.2 → 0.7.5

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 (33) hide show
  1. package/MANUAL.md +1 -1
  2. package/README.md +11 -1
  3. package/RELEASE.md +16 -0
  4. package/assets/agents/developer.md +2 -1
  5. package/assets/agents/slopmachine-claude.md +28 -20
  6. package/assets/agents/slopmachine.md +22 -18
  7. package/assets/claude/agents/developer.md +2 -1
  8. package/assets/skills/beads-operations/SKILL.md +1 -1
  9. package/assets/skills/clarification-gate/SKILL.md +6 -4
  10. package/assets/skills/claude-worker-management/SKILL.md +6 -6
  11. package/assets/skills/developer-session-lifecycle/SKILL.md +11 -9
  12. package/assets/skills/development-guidance/SKILL.md +1 -0
  13. package/assets/skills/evaluation-triage/SKILL.md +3 -2
  14. package/assets/skills/final-evaluation-orchestration/SKILL.md +12 -19
  15. package/assets/skills/hardening-gate/SKILL.md +1 -0
  16. package/assets/skills/planning-guidance/SKILL.md +1 -0
  17. package/assets/skills/scaffold-guidance/SKILL.md +1 -0
  18. package/assets/skills/submission-packaging/SKILL.md +14 -11
  19. package/assets/skills/verification-gates/SKILL.md +5 -4
  20. package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +4 -0
  21. package/assets/slopmachine/templates/AGENTS.md +3 -1
  22. package/assets/slopmachine/templates/CLAUDE.md +3 -1
  23. package/assets/slopmachine/utils/__pycache__/normalize_claude_session.cpython-311.pyc +0 -0
  24. package/assets/slopmachine/utils/claude_live_launch.mjs +2 -2
  25. package/assets/slopmachine/utils/normalize_claude_session.py +162 -27
  26. package/assets/slopmachine/utils/package_claude_session.mjs +120 -23
  27. package/assets/slopmachine/utils/prepare_evaluation_prompt.mjs +41 -0
  28. package/assets/slopmachine/workflow-init.js +1 -1
  29. package/package.json +1 -1
  30. package/src/cli.js +1 -1
  31. package/src/constants.js +1 -0
  32. package/src/init.js +117 -28
  33. package/src/send-data.js +4 -4
@@ -60,6 +60,7 @@ Hardening should treat these as the main review buckets before final evaluation
60
60
  - enforce env-file discipline during hardening
61
61
  - run documentation verification against the real codebase and runtime behavior, not just document existence
62
62
  - if `README.md` declares containerized runtime or broad test commands, verify that the final delivered output really supports those exact commands and that the docs do not overpromise beyond what the repo actually does
63
+ - verify that every dependency needed by the README-documented `docker compose up --build` and `./run_tests.sh` paths is declared in Dockerfiles or other repo-controlled container build definitions rather than relying on host-installed packages or runtimes
63
64
  - audit README compliance against the strict post-bugfix README review shape:
64
65
  - project type near the top
65
66
  - startup instructions
@@ -163,6 +163,7 @@ Selected-stack defaults:
163
163
  - `./run_tests.sh` must exist for every project as the platform-independent broad test wrapper
164
164
  - `./run_tests.sh` must be able to run on a clean Linux VM that only has Docker and curl available by default
165
165
  - do not require host package managers, host language runtimes, or host test tooling for the broad test path unless the stack absolutely forces it and the exception is explicitly justified
166
+ - define all runtime and broad-test dependencies in Dockerfiles, image build stages, or other repo-controlled container build definitions; do not rely on hidden host packages even when the developer machine happens to have them
166
167
  - `./run_tests.sh` must prepare or install anything required inside its own controlled execution path when that setup is needed for a clean environment
167
168
  - for web projects, `./run_tests.sh` must run the full test path through Docker rather than a purely local test invocation
168
169
  - when host-level setup would otherwise be required, prefer a Dockerized `./run_tests.sh` path even outside traditional web stacks so the broad verification remains portable
@@ -90,6 +90,7 @@ At scaffold time, do not require:
90
90
  - create `./run_tests.sh` during scaffold for every project
91
91
  - create `./run_app.sh` during scaffold for non-web platforms when it helps expose the host-side or platform-specific local flow, but keep `docker compose up --build` and containerized `./run_tests.sh` as required baseline commands
92
92
  - if the project has database dependencies, create `./init_db.sh` during scaffold as the only project-standard database initialization path
93
+ - define the runtime and broad-test dependency set in Dockerfiles or other repo-controlled container build definitions during scaffold; do not assume host package managers, host SDKs, or host language runtimes beyond Docker and documented baseline prerequisites
93
94
  - make the scaffold handoff compact and checklist-driven rather than a long narrative dump
94
95
 
95
96
  ## Minimal real test floor
@@ -17,7 +17,7 @@ Use this skill only during `P9 Submission Packaging`.
17
17
  - packaging is incomplete until every required final artifact path has been verified to exist
18
18
  - do not stop packaging for approval, status confirmation, or handoff once this phase has begun; continue until the package is complete
19
19
  - when a task or platform question id exists such as `TASK-123`, use that exact id as the final deliverable/archive name without adding an extra `ID-` prefix
20
- - normalize project-type metadata and packaging labels to the expected engineering categories such as `full_stack` or `fullstack`, `pure_backend`, `pure_frontend`, `cross_platform_app`, or `mobile_app`
20
+ - normalize `metadata.json.project_type` to the exact allowed values `fullstack`, `backend`, `android`, `ios`, `desktop`, or `web`, and keep any packaging labels aligned with that same delivered project reality
21
21
 
22
22
  ## Required final structure
23
23
 
@@ -32,16 +32,16 @@ The final delivery layout in the parent project root must be:
32
32
  - `sessions/`
33
33
  - `sessions/<label>.json` for every tracked developer session, including `develop-N.json` and `bugfix-N.json` when present
34
34
  - for Claude-backed developer sessions:
35
- - `claude-sessions.zip` in the parent root containing the whole Claude project session folder once
35
+ - `claude-sessions.zip` in the parent root containing only the tracked relevant Claude session artifacts once
36
36
  - no `sessions/` directory is required when all tracked developer sessions are Claude-backed
37
37
  - `metadata.json`
38
38
  - `.tmp/`
39
39
  - `audit_report-<N>.md` only for bugfix-triggering `partial pass` audits
40
- - `audit_report-<N>-fix_check-<M>.md` when present
40
+ - `audit_report-<N>-fix_check.md` when present
41
41
  - `test_coverage_and_readme_audit_report.md`
42
42
  - `repo/`
43
43
 
44
- In the clean two-bugfix path, `.tmp/` should end with at least 5 required markdown reports once the final coverage/README audit is included: 2 kept partial-pass audit reports, at least 2 corresponding fix-check reports, and the final coverage/README audit report. Extra fix checks may legitimately increase that count.
44
+ In the clean two-bugfix path, `.tmp/` should end with at least 5 required markdown reports once the final coverage/README audit is included: 2 kept partial-pass audit reports, 2 corresponding single fix-check reports, and the final coverage/README audit report.
45
45
 
46
46
  Inside the delivered `repo/`, the repository must remain self-sufficient:
47
47
 
@@ -56,7 +56,7 @@ No screenshots are required as packaging artifacts.
56
56
  ## Required packaging actions
57
57
 
58
58
  - verify the parent-root package structure matches the blueprint exactly
59
- - make sure parent-root `../metadata.json` is complete and reflects the delivered project truthfully
59
+ - make sure parent-root `../metadata.json` is complete, reflects the delivered project truthfully, and contains only these keys: `prompt`, `project_type`, `frontend_language`, `backend_language`, `database`, `frontend_framework`, `backend_framework`
60
60
  - verify parent-root `../docs/design.md` exists and reflects the final delivered design when applicable
61
61
  - verify parent-root `../docs/api-spec.md` exists and reflects the final delivered interfaces when applicable
62
62
  - verify parent-root `../docs/test-coverage.md` exists and reflects the final delivered verification coverage
@@ -74,7 +74,7 @@ No screenshots are required as packaging artifacts.
74
74
  - verify parent-root `../.tmp/` exists and contains the required audit and fix-check reports
75
75
  - verify parent-root `../.tmp/test_coverage_and_readme_audit_report.md` exists from the final post-bugfix coverage/README audit
76
76
  - export all tracked developer sessions before closing packaging
77
- - when packaging succeeds, update workflow metadata to mark `packaging_completed` as true
77
+ - when packaging succeeds and any tracked live Claude tmux lanes have been stopped, update workflow metadata to mark `packaging_completed` as true
78
78
 
79
79
  ## Session export sequence
80
80
 
@@ -85,18 +85,20 @@ Export tracked developer sessions from metadata using the tracked lane labels, f
85
85
 
86
86
  For session export:
87
87
 
88
- 1. if at least one tracked developer session backend is `claude` or `claude-live`, run `node ~/slopmachine/utils/package_claude_session.mjs --cwd "$PWD" --session-id <any-claude-session-id> --label claude-sessions --output ../claude-sessions.zip`
88
+ 1. if at least one tracked developer session backend is `claude` or `claude-live`, gather the tracked Claude `session_id` values from metadata and run `node ~/slopmachine/utils/package_claude_session.mjs --cwd "$PWD" --session-ids <tracked-claude-session-id-1,tracked-claude-session-id-2,...> --label claude-sessions --output ../claude-sessions.zip`
89
89
  2. if `<backend>` is neither `claude` nor `claude-live`, run `opencode export <session-id> > ../session-export-<label>.raw`
90
90
  3. if `<backend>` is neither `claude` nor `claude-live`, run `python3 ~/slopmachine/utils/strip_session_parent.py ../session-export-<label>.raw --output ../sessions/<label>.json`
91
91
 
92
92
  Where `<backend>` comes from the tracked developer session record in metadata.
93
93
  Use `opencode` when no explicit backend field exists or when the backend is not Claude-backed.
94
- For Claude-backed sessions, the package helper resolves the Claude project folder under `~/.claude/projects/` from a tracked `session_id` plus the current project `cwd`, normalizes the copied JSONL session files by flattening channel-originated user turns, and packages that folder once.
94
+ For Claude-backed sessions, the package helper resolves the tracked Claude session artifacts under `~/.claude/projects/` from the tracked `session_id` values plus the current project `cwd`, copies only those tracked `session_id.jsonl` files and matching `session_id/` companion directories when present, normalizes the copied JSONL session files by flattening channel-originated user turns, and packages only that tracked set once. Do not sweep unrelated random Claude sessions into the archive just because they share the same Claude project directory.
95
95
 
96
96
  After those steps:
97
97
 
98
98
  - verify every non-Claude developer session has been exported into `../sessions/<label>.json`
99
- - verify Claude-backed sessions have been packaged once into `../claude-sessions.zip`
99
+ - verify Claude-backed sessions have been packaged once into `../claude-sessions.zip` using the tracked relevant Claude session ids rather than the whole local Claude project directory
100
+ - after Claude-backed session packaging succeeds, stop each tracked live Claude runtime with `node ~/slopmachine/utils/claude_live_stop.mjs --runtime-dir <runtime_dir>` before marking packaging complete
101
+ - verify each stopped Claude runtime no longer has a live tmux session before closing packaging
100
102
  - treat only the raw `../session-export-<label>.raw` files as temporary packaging intermediates
101
103
  - remove the raw `../session-export-<label>.raw` files before closing packaging
102
104
  - if the required utilities, metadata session ids, or output files are missing, packaging is not ready to continue
@@ -127,13 +129,14 @@ After those steps:
127
129
  - confirm the cleanup helper has been run and that no known recursive cleanup targets remain in the delivered repo tree
128
130
  - confirm no environment-dependent dependency directories, editor-state folders, runtime caches, or workflow utility scripts are packaged into the delivered product
129
131
  - confirm parent-root `../.tmp/` exists and contains the required kept `audit_report-<N>.md` files for partial-pass audits only
130
- - confirm every bugfix-triggering audit number has its matching `audit_report-<N>-fix_check-<M>.md` files when fix checks were required
132
+ - confirm every bugfix-triggering audit number has its matching `audit_report-<N>-fix_check.md` file when fix checks were required
131
133
  - confirm parent-root `../.tmp/test_coverage_and_readme_audit_report.md` exists and is the final replaced copy rather than a numbered variant
132
134
  - confirm parent-root `../docs/test-coverage.md` explains the tested flows, mapped tests, and coverage boundaries
133
135
  - confirm every non-Claude developer session exists under parent-root `../sessions/` using the tracked `<label>.json` names
134
136
  - confirm Claude-backed developer sessions exist in the parent root as `claude-sessions.zip`
137
+ - confirm no tracked Claude live tmux session is still running after packaging finishes
135
138
  - confirm parent-root `../docs/` remains consistent as an external reference set when workflow policy still requires it, but the delivered repo does not depend on it
136
- - confirm parent-root metadata fields are populated correctly
139
+ - confirm parent-root metadata fields are populated correctly and no extra keys exist in `../metadata.json`
137
140
  - confirm workflow metadata marks `packaging_completed` as true
138
141
  - confirm no `submission/` directory or other obsolete packaging artifact structure remains
139
142
 
@@ -12,8 +12,8 @@ Use this skill after development begins whenever you are reviewing work, decidin
12
12
  - load this skill before review, acceptance, rejection, runtime gate interpretation, hardening readiness decisions, or broad-gate decisions
13
13
  - treat it as owner-side review and gate guidance, not developer-visible text
14
14
  - use this skill as the source of truth for owner-side verification, review pressure, and gate interpretation
15
- - outside the final human decision, do not pause execution for human approval while using this skill; continue reviewing, rejecting, fixing, and rerunning until the work qualifies
16
- - outside the clarification-complete gate and `P8 Final Human Decision`, do not pause execution just to summarize progress or ask the user whether to continue; keep driving the workflow until the current gate or phase objective is actually satisfied
15
+ - do not pause execution for human approval while using this skill; continue reviewing, rejecting, fixing, and rerunning until the work qualifies
16
+ - clarification completion and `P8 Final Readiness Decision` are internal workflow transitions, not user-stop gates; do not pause execution just to summarize progress or ask the user whether to continue
17
17
 
18
18
  ## Documentation and repo hygiene
19
19
 
@@ -48,6 +48,7 @@ Use this skill after development begins whenever you are reviewing work, decidin
48
48
  - for Android, mobile, desktop, and iOS-targeted projects, allow `./run_app.sh` as an additional platform helper but not as a replacement for the required Docker command
49
49
  - require `./run_tests.sh` to be self-sufficient enough to run from a clean Linux VM that only has Docker and curl available by default
50
50
  - do not accept a broad test path that depends on host package managers or preinstalled host language runtimes when Docker can provide the execution environment instead
51
+ - do not accept Docker runtime/test paths that rely on undeclared host packages, SDKs, compilers, CLIs, or language runtimes; all such dependencies must be defined in Dockerfiles or other repo-controlled container build definitions
51
52
  - for web projects, require `./run_tests.sh` to be the Dockerized broad test path used only for the limited broad verification moments rather than as the ordinary development verification path
52
53
  - when host-level setup would otherwise be required, prefer a Dockerized `./run_tests.sh` path even outside traditional web stacks so the broad verification remains portable
53
54
  - for non-web projects, require `./run_tests.sh` to remain containerized and usable as the platform-equivalent broad test path used for final broad verification
@@ -162,7 +163,7 @@ Any earlier extra Docker run needs a concrete blocker-based justification.
162
163
 
163
164
  Use evidence such as internal metadata files, structured Beads comments, verification command results, and file/project-state checks.
164
165
 
165
- - clarification requires the `clarification-gate` conditions plus explicit approval record
166
+ - clarification requires the `clarification-gate` conditions plus an internally accepted clarification record that is ready to roll directly into planning
166
167
  - planning requires the `developer-session-lifecycle` and planning-gate conditions plus a fresh planning-oriented start and the required documentation and repo hygiene state when relevant
167
168
  - planning exit also requires explicit owner review that the accepted planning artifacts cover the section-addressable contract deeply enough for later implementation: in-scope and out-of-scope, actors and success paths, modules, business rules, state machines, permissions, validation, verification strategy, checkpoints, and definition of done when applicable
168
169
  - planning exit does not pass if those sections exist only nominally or remain too vague to drive implementation without broad reinvention
@@ -213,7 +214,7 @@ Use evidence such as internal metadata files, structured Beads comments, verific
213
214
  - before `P7`, for `fullstack` and `web` projects, require an explicit frontend unit-test verdict backed by direct file-level evidence; if frontend unit tests are missing or insufficient, treat that as a critical gap
214
215
  - before `P7`, require repo-local build/preview/config traceability plus disclosure in `README.md` of feature flags, debug/demo surfaces, and mock defaults when those surfaces exist
215
216
  - before `P7`, require logging and validation contracts to be statically traceable enough that the owner can review them from the repo plus external references when needed
216
- - final evaluation readiness requires the audit-numbered `P7` model under `../.tmp/`; only `partial pass` fresh evaluations leave persisted `audit_report-<N>.md` files, `fail` audits route back to the latest `develop-N` session and discard their working report after triage, `pass` audits discard their working report and rerun fresh evaluation, `partial pass` audits open scoped `bugfix-N` sessions whose fix checks are stored as `audit_report-<N>-fix_check-<M>.md`, and the last subphase of `P7` runs `test_coverage_and_readme_audit_report.md` with up to 3 remediation attempts before carrying the latest report forward
217
+ - final evaluation readiness requires the audit-numbered `P7` model under `../.tmp/`; only `partial pass` fresh evaluations leave persisted `audit_report-<N>.md` files, `fail` audits route back to the latest `develop-N` session and discard their working report after triage, `pass` audits discard their working report and rerun fresh evaluation, `partial pass` audits open scoped `bugfix-N` sessions whose fix checks are stored in a single replace-in-place `audit_report-<N>-fix_check.md`, and the last subphase of `P7` runs `test_coverage_and_readme_audit_report.md` with up to 3 remediation attempts before carrying the latest report forward
217
218
  - before leaving `P7`, if `README.md` documents `docker compose up --build` and/or `./run_tests.sh` as part of the delivered external contract, run those exact commands on the final state and require them to pass before moving to `P8`
218
219
  - if the `P7` issue-fix loop materially reopens the integrated verification boundary, route it back through integrated verification before continuing with follow-up fix verification
219
220
  - before leaving `P7`, require the parent-root `../.tmp/test_coverage_and_readme_audit_report.md` to exist from the last `P7` subphase; if it finds issues, route the fixes to the currently active recoverable developer session, replace the report, and rerun the audit, but stop after 3 remediation attempts and keep the latest report as the final carried-forward evidence
@@ -49,6 +49,7 @@ That proof differs by family, but it must always be **meaningful**:
49
49
  At minimum, `docker compose up --build` must prove all of the following:
50
50
 
51
51
  - images build from source in the repo
52
+ - all system packages, language runtimes, CLIs, compilers, and SDKs required by the runtime path are declared in Dockerfiles or image build stages rather than assumed from the host
52
53
  - declared runtime or support services start without hidden host setup
53
54
  - readiness can be observed through healthchecks, predictable logs, or explicit successful artifact production
54
55
  - the stack reaches a meaningful healthy state without manual container surgery
@@ -71,6 +72,9 @@ It must prove that the scaffold can be verified from a clean-enough developer or
71
72
  - run smoke checks against the actual baseline contract
72
73
  - clean up one-off test containers and temporary runtime state
73
74
 
75
+ `docker compose up --build` and `./run_tests.sh` must never depend on host-installed language runtimes, package managers, CLIs, compilers, SDKs, or other local system packages beyond Docker and the documented baseline host prerequisites.
76
+ If a dependency is needed for runtime or broad verification, define it in Dockerfiles, image build stages, or other repo-controlled container build definitions.
77
+
74
78
  `./run_tests.sh` must not be a fake wrapper that only prints TODOs, only runs lint when runtime proof is expected, or silently skips the meaningful part of verification.
75
79
 
76
80
  Minimum proof by family:
@@ -15,6 +15,7 @@ This file is the repo-local engineering rulebook for `slopmachine` projects.
15
15
  - Read the code before making assumptions.
16
16
  - Work in meaningful vertical slices.
17
17
  - Do not call work complete while it is still shaky.
18
+ - Once given a bounded objective, keep going autonomously until it is complete or genuinely blocked; do not stop for reassurance or permission when a prompt-faithful default lets you proceed.
18
19
  - Reuse and extend shared cross-cutting patterns instead of inventing incompatible local ones.
19
20
  - Before coding, identify the actors or personas touched by the change and the concrete path to success for each one.
20
21
  - Make important business rules explicit before coding: defaults, limits, allowed transitions, uniqueness, conflicts, reversals, retries, and ownership rules when they matter.
@@ -28,7 +29,7 @@ This file is the repo-local engineering rulebook for `slopmachine` projects.
28
29
 
29
30
  - Preserve the full prompt intent, including implied business constraints.
30
31
  - Do not weaken required actor models, operator flows, security controls, or lifecycle behavior for implementation convenience.
31
- - If a requirement is ambiguous, choose the safest prompt-faithful behavior or surface the ambiguity instead of guessing lazily.
32
+ - If a requirement is ambiguous, choose the safest prompt-faithful behavior and keep moving when a defensible default exists; surface the ambiguity only when it is genuinely blocking or materially changes the product contract.
32
33
  - If the feature depends on business rules, make those rules traceable in code, tests, and `README.md` rather than leaving them implicit.
33
34
 
34
35
  ## Architecture Rules
@@ -88,6 +89,7 @@ This file is the repo-local engineering rulebook for `slopmachine` projects.
88
89
  - `./run_tests.sh` should use the same startup-value model as `docker compose up --build` rather than a separate pre-seeded test-secret path.
89
90
  - If local-development runtime values must persist across restarts, keep them only in Docker-managed runtime state rather than committed repo files.
90
91
  - If such a bootstrap script exists, document in the script and in `README.md` that it is for local development bootstrap only and is not the production secret-management path.
92
+ - Do not let `docker compose up --build` or `./run_tests.sh` depend on host-installed packages, SDKs, language runtimes, CLIs, or toolchains beyond Docker and the documented baseline host prerequisites; define those dependencies in Dockerfiles or other repo-controlled container build definitions.
91
93
 
92
94
  ## Product Integrity Rules
93
95
 
@@ -15,6 +15,7 @@ This file is the repo-local engineering rulebook for `slopmachine-claude` projec
15
15
  - Read the code before making assumptions.
16
16
  - Work in meaningful vertical slices.
17
17
  - Do not call work complete while it is still shaky.
18
+ - Once given a bounded objective, keep going autonomously until it is complete or genuinely blocked; do not stop for reassurance or permission when a prompt-faithful default lets you proceed.
18
19
  - Reuse and extend shared cross-cutting patterns instead of inventing incompatible local ones.
19
20
  - Before coding, identify the actors or personas touched by the change and the concrete path to success for each one.
20
21
  - Make important business rules explicit before coding: defaults, limits, allowed transitions, uniqueness, conflicts, reversals, retries, and ownership rules when they matter.
@@ -28,7 +29,7 @@ This file is the repo-local engineering rulebook for `slopmachine-claude` projec
28
29
 
29
30
  - Preserve the full prompt intent, including implied business constraints.
30
31
  - Do not weaken required actor models, operator flows, security controls, or lifecycle behavior for implementation convenience.
31
- - If a requirement is ambiguous, choose the safest prompt-faithful behavior or surface the ambiguity instead of guessing lazily.
32
+ - If a requirement is ambiguous, choose the safest prompt-faithful behavior and keep moving when a defensible default exists; surface the ambiguity only when it is genuinely blocking or materially changes the product contract.
32
33
  - If the feature depends on business rules, make those rules traceable in code, tests, and `README.md` rather than leaving them implicit.
33
34
 
34
35
  ## Architecture Rules
@@ -88,6 +89,7 @@ This file is the repo-local engineering rulebook for `slopmachine-claude` projec
88
89
  - `./run_tests.sh` should use the same startup-value model as `docker compose up --build` rather than a separate pre-seeded test-secret path.
89
90
  - If local-development runtime values must persist across restarts, keep them only in Docker-managed runtime state rather than committed repo files.
90
91
  - If such a bootstrap script exists, document in the script and in `README.md` that it is for local development bootstrap only and is not the production secret-management path.
92
+ - Do not let `docker compose up --build` or `./run_tests.sh` depend on host-installed packages, SDKs, language runtimes, CLIs, or toolchains beyond Docker and the documented baseline host prerequisites; define those dependencies in Dockerfiles or other repo-controlled container build definitions.
91
93
 
92
94
  ## Product Integrity Rules
93
95
 
@@ -36,8 +36,8 @@ const cwd = argv.cwd ? path.resolve(argv.cwd) : null
36
36
  const lane = argv.lane
37
37
  const agentName = argv.agent || 'developer'
38
38
  const claudeCommand = argv['claude-command'] || 'claude'
39
- const laneModel = argv.model || 'sonnet'
40
- const laneEffort = argv.effort || null
39
+ const laneModel = argv.model || 'opus'
40
+ const laneEffort = argv.effort || 'medium'
41
41
  const subagentModel = argv['subagent-model'] || 'sonnet'
42
42
  const launchTimeoutMs = Number.parseInt(argv['timeout-ms'] || String(DEFAULT_LAUNCH_TIMEOUT_MS), 10)
43
43
  const replace = argv.replace === '1'
@@ -7,7 +7,7 @@ import json
7
7
  import re
8
8
  import sys
9
9
  from pathlib import Path
10
- from typing import Any
10
+ from typing import Any, Iterable
11
11
 
12
12
 
13
13
  CHANNEL_MESSAGE_RE = re.compile(
@@ -15,6 +15,10 @@ CHANNEL_MESSAGE_RE = re.compile(
15
15
  re.DOTALL,
16
16
  )
17
17
 
18
+ CHANNEL_INSTRUCTION_RE = re.compile(r"slopmachine-[^\s\"]+", re.IGNORECASE)
19
+ CHANNEL_TAG_RE = re.compile(r"</?channel\b[^>]*>", re.IGNORECASE)
20
+ WEBHOOK_TERM_RE = re.compile(r"\b(?:webhook|UserPromptSubmit|StopFailure)\b", re.IGNORECASE)
21
+
18
22
 
19
23
  def parse_args() -> argparse.Namespace:
20
24
  parser = argparse.ArgumentParser(
@@ -37,6 +41,11 @@ def parse_args() -> argparse.Namespace:
37
41
  action="store_true",
38
42
  help="Keep channel origin metadata fields instead of stripping them",
39
43
  )
44
+ parser.add_argument(
45
+ "--recursive",
46
+ action="store_true",
47
+ help="If input is a directory, recursively normalize all .jsonl files into the output directory.",
48
+ )
40
49
  return parser.parse_args()
41
50
 
42
51
 
@@ -49,55 +58,137 @@ def maybe_flatten_channel_content(content: str) -> tuple[str, bool]:
49
58
  return body, True
50
59
 
51
60
 
61
+ def scrub_string(value: str) -> str:
62
+ flattened, _ = maybe_flatten_channel_content(value.strip())
63
+ normalized = flattened
64
+ normalized = CHANNEL_TAG_RE.sub("", normalized)
65
+ normalized = CHANNEL_INSTRUCTION_RE.sub("normalized-channel", normalized)
66
+ normalized = WEBHOOK_TERM_RE.sub("", normalized)
67
+ normalized = normalized.replace(
68
+ 'Messages arrive as ordinary inbound work requests. ...',
69
+ 'Messages arrive as ordinary inbound work requests.',
70
+ )
71
+ return normalized
72
+
73
+
74
+ def scrub_value(value: Any) -> Any:
75
+ if isinstance(value, str):
76
+ return scrub_string(value)
77
+ if isinstance(value, list):
78
+ return [scrub_value(item) for item in value]
79
+ if isinstance(value, dict):
80
+ return {key: scrub_value(item) for key, item in value.items()}
81
+ return value
82
+
83
+
84
+ def strip_channel_instruction_attachment(attachment: dict[str, Any]) -> tuple[dict[str, Any] | None, bool]:
85
+ if attachment.get("type") != "mcp_instructions_delta":
86
+ return attachment, False
87
+
88
+ changed = False
89
+ normalized = dict(attachment)
90
+
91
+ added_names = normalized.get("addedNames")
92
+ if isinstance(added_names, list):
93
+ filtered_names = [name for name in added_names if not (isinstance(name, str) and name.startswith("slopmachine-"))]
94
+ if filtered_names != added_names:
95
+ normalized["addedNames"] = filtered_names
96
+ changed = True
97
+
98
+ added_blocks = normalized.get("addedBlocks")
99
+ if isinstance(added_blocks, list):
100
+ filtered_blocks = []
101
+ for block in added_blocks:
102
+ if not isinstance(block, str):
103
+ filtered_blocks.append(block)
104
+ continue
105
+ if "<channel source=" in block or "Messages arrive as <channel source=" in block or CHANNEL_INSTRUCTION_RE.search(block):
106
+ changed = True
107
+ continue
108
+ filtered_blocks.append(block)
109
+ if filtered_blocks != added_blocks:
110
+ normalized["addedBlocks"] = filtered_blocks
111
+
112
+ removed_names = normalized.get("removedNames")
113
+ if isinstance(removed_names, list) and not normalized.get("addedNames") and not normalized.get("addedBlocks") and not removed_names:
114
+ return None, True
115
+
116
+ if not normalized.get("addedNames") and not normalized.get("addedBlocks") and not normalized.get("removedNames"):
117
+ return None, True
118
+
119
+ return scrub_value(normalized), changed
120
+
121
+
122
+ def strip_transport_metadata(record: dict[str, Any], *, keep_channel_origin: bool) -> dict[str, Any]:
123
+ normalized = dict(record)
124
+ normalized.pop("isMeta", None)
125
+ if not keep_channel_origin:
126
+ normalized.pop("origin", None)
127
+ return scrub_value(normalized)
128
+
129
+
52
130
  def normalize_record(record: dict[str, Any], *, keep_channel_origin: bool) -> dict[str, Any] | None:
53
131
  if record.get("type") == "queue-operation":
54
132
  return None
55
133
 
134
+ if record.get("type") == "attachment":
135
+ attachment = record.get("attachment")
136
+ if isinstance(attachment, dict):
137
+ cleaned_attachment, _ = strip_channel_instruction_attachment(attachment)
138
+ if cleaned_attachment is None:
139
+ return None
140
+ normalized = strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
141
+ normalized["attachment"] = cleaned_attachment
142
+ return normalized
143
+ return strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
144
+
56
145
  if record.get("type") != "user":
57
- return record
146
+ return strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
58
147
 
59
148
  message = record.get("message")
60
149
  if not isinstance(message, dict):
61
- return record
150
+ return strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
62
151
 
63
152
  if message.get("role") != "user":
64
- return record
153
+ return strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
65
154
 
66
155
  content = message.get("content")
67
156
  if not isinstance(content, str):
68
- return record
157
+ return strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
69
158
 
70
159
  flattened, changed = maybe_flatten_channel_content(content)
160
+ normalized = strip_transport_metadata(record, keep_channel_origin=keep_channel_origin)
71
161
  if not changed:
72
- return record
162
+ return normalized
73
163
 
74
- normalized = dict(record)
75
164
  normalized_message = dict(message)
76
165
  normalized_message["content"] = flattened
77
166
  normalized["message"] = normalized_message
78
- normalized.pop("isMeta", None)
79
-
80
- if not keep_channel_origin:
81
- normalized.pop("origin", None)
82
167
 
83
168
  return normalized
84
169
 
85
170
 
86
- def main() -> int:
87
- args = parse_args()
88
- input_path = Path(args.input)
89
- output_path = Path(args.output)
171
+ def iter_input_files(input_path: Path, recursive: bool) -> Iterable[Path]:
172
+ if input_path.is_file():
173
+ yield input_path
174
+ return
90
175
 
91
- if not input_path.exists():
92
- print(f"Input file not found: {input_path}", file=sys.stderr)
93
- return 1
176
+ if not input_path.is_dir():
177
+ raise ValueError(f"Input path must be a file or directory: {input_path}")
94
178
 
95
- output_path.parent.mkdir(parents=True, exist_ok=True)
179
+ pattern = "**/*.jsonl" if recursive else "*.jsonl"
180
+ for candidate in sorted(input_path.glob(pattern)):
181
+ if candidate.is_file():
182
+ yield candidate
96
183
 
184
+
185
+ def normalize_file(input_path: Path, output_path: Path, *, keep_channel_origin: bool, keep_queue_operations: bool) -> dict[str, Any]:
97
186
  total = 0
98
187
  queue_dropped = 0
99
188
  channel_flattened = 0
100
189
 
190
+ output_path.parent.mkdir(parents=True, exist_ok=True)
191
+
101
192
  with input_path.open("r", encoding="utf-8") as src, output_path.open("w", encoding="utf-8") as dst:
102
193
  for line_no, line in enumerate(src, start=1):
103
194
  stripped = line.strip()
@@ -108,17 +199,15 @@ def main() -> int:
108
199
  try:
109
200
  record = json.loads(stripped)
110
201
  except json.JSONDecodeError as exc:
111
- print(f"Invalid JSON at line {line_no}: {exc}", file=sys.stderr)
112
- return 1
202
+ raise ValueError(f"Invalid JSON at line {line_no} in {input_path}: {exc}") from exc
113
203
 
114
204
  if not isinstance(record, dict):
115
- print(f"Expected object at line {line_no}", file=sys.stderr)
116
- return 1
205
+ raise ValueError(f"Expected object at line {line_no} in {input_path}")
117
206
 
118
- normalized = normalize_record(record, keep_channel_origin=args.keep_channel_origin)
207
+ normalized = normalize_record(record, keep_channel_origin=keep_channel_origin)
119
208
 
120
209
  if normalized is None:
121
- if record.get("type") == "queue-operation" and not args.keep_queue_operations:
210
+ if record.get("type") == "queue-operation" and not keep_queue_operations:
122
211
  queue_dropped += 1
123
212
  continue
124
213
 
@@ -137,14 +226,60 @@ def main() -> int:
137
226
 
138
227
  dst.write(json.dumps(normalized, ensure_ascii=False) + "\n")
139
228
 
140
- summary = {
141
- "ok": True,
229
+ return {
142
230
  "input": str(input_path),
143
231
  "output": str(output_path),
144
232
  "records_seen": total,
145
233
  "queue_operations_dropped": queue_dropped,
146
234
  "channel_messages_flattened": channel_flattened,
147
235
  }
236
+
237
+
238
+ def main() -> int:
239
+ args = parse_args()
240
+ input_path = Path(args.input)
241
+ output_path = Path(args.output)
242
+
243
+ if not input_path.exists():
244
+ print(f"Input file not found: {input_path}", file=sys.stderr)
245
+ return 1
246
+
247
+ try:
248
+ files = list(iter_input_files(input_path, recursive=args.recursive))
249
+ except ValueError as exc:
250
+ print(str(exc), file=sys.stderr)
251
+ return 1
252
+
253
+ summaries = []
254
+ for source in files:
255
+ if input_path.is_dir():
256
+ rel = source.relative_to(input_path)
257
+ dest = output_path / rel
258
+ else:
259
+ dest = output_path
260
+ try:
261
+ summaries.append(
262
+ normalize_file(
263
+ source,
264
+ dest,
265
+ keep_channel_origin=args.keep_channel_origin,
266
+ keep_queue_operations=args.keep_queue_operations,
267
+ )
268
+ )
269
+ except ValueError as exc:
270
+ print(str(exc), file=sys.stderr)
271
+ return 1
272
+
273
+ summary = {
274
+ "ok": True,
275
+ "input": str(input_path),
276
+ "output": str(output_path),
277
+ "files_processed": len(summaries),
278
+ "records_seen": sum(item["records_seen"] for item in summaries),
279
+ "queue_operations_dropped": sum(item["queue_operations_dropped"] for item in summaries),
280
+ "channel_messages_flattened": sum(item["channel_messages_flattened"] for item in summaries),
281
+ "per_file": summaries,
282
+ }
148
283
  print(json.dumps(summary))
149
284
  return 0
150
285