theslopmachine 0.7.3 → 0.7.6
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.
- package/MANUAL.md +1 -1
- package/README.md +13 -1
- package/RELEASE.md +16 -0
- package/assets/agents/developer.md +2 -1
- package/assets/agents/slopmachine-claude.md +31 -20
- package/assets/agents/slopmachine.md +22 -18
- package/assets/claude/agents/developer.md +2 -1
- package/assets/skills/beads-operations/SKILL.md +1 -1
- package/assets/skills/clarification-gate/SKILL.md +6 -4
- package/assets/skills/claude-worker-management/SKILL.md +43 -6
- package/assets/skills/developer-session-lifecycle/SKILL.md +13 -9
- package/assets/skills/evaluation-triage/SKILL.md +3 -2
- package/assets/skills/final-evaluation-orchestration/SKILL.md +12 -19
- package/assets/skills/submission-packaging/SKILL.md +14 -11
- package/assets/skills/verification-gates/SKILL.md +4 -4
- package/assets/slopmachine/templates/AGENTS.md +2 -1
- package/assets/slopmachine/templates/CLAUDE.md +2 -1
- package/assets/slopmachine/utils/__pycache__/normalize_claude_session.cpython-311.pyc +0 -0
- package/assets/slopmachine/utils/claude_live_common.mjs +45 -6
- package/assets/slopmachine/utils/claude_live_launch.mjs +4 -4
- package/assets/slopmachine/utils/normalize_claude_session.py +162 -27
- package/assets/slopmachine/utils/package_claude_session.mjs +120 -23
- package/assets/slopmachine/utils/prepare_evaluation_prompt.mjs +41 -0
- package/assets/slopmachine/workflow-init.js +21 -1
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/constants.js +1 -0
- package/src/init.js +118 -28
- package/src/send-data.js +4 -4
|
@@ -43,10 +43,8 @@ The installed runtime copies under `~/slopmachine/` are the ordinary evaluation
|
|
|
43
43
|
- number every fresh evaluation audit sequentially across the whole run for routing and metadata purposes
|
|
44
44
|
- persist `../.tmp/audit_report-<N>.md` only for `partial pass` audits that actually open bugfix sessions
|
|
45
45
|
- if a fresh audit is `fail` or `pass`, extract what you need from the generated working report, record the verdict and routing in metadata, and then discard the report file instead of leaving it in `../.tmp/`
|
|
46
|
-
- for a `partial pass` audit that opens a bugfix session,
|
|
47
|
-
- `../.tmp/audit_report-<N>-fix_check
|
|
48
|
-
- `../.tmp/audit_report-<N>-fix_check-2.md`
|
|
49
|
-
- and so on
|
|
46
|
+
- for a `partial pass` audit that opens a bugfix session, keep one replace-in-place fix-check report under that audit number:
|
|
47
|
+
- `../.tmp/audit_report-<N>-fix_check.md`
|
|
50
48
|
|
|
51
49
|
## Evaluator-session model
|
|
52
50
|
|
|
@@ -72,18 +70,14 @@ The installed runtime copies under `~/slopmachine/` are the ordinary evaluation
|
|
|
72
70
|
|
|
73
71
|
For each fresh audit:
|
|
74
72
|
|
|
75
|
-
-
|
|
76
|
-
- use the original project prompt from metadata
|
|
73
|
+
- do not prefix, suffix, summarize, or otherwise rewrite the chosen evaluation prompt yourself
|
|
77
74
|
- read the chosen evaluation prompt file contents yourself before launching evaluation
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
- inject the full original project prompt into the `{prompt}` placeholder for the chosen evaluation prompt content, but otherwise do not rewrite or replace the template body
|
|
82
|
-
- send that fully composed text block directly to one fresh `General` evaluator session
|
|
83
|
-
- require that session to produce a detailed file-backed audit report plus an issue summary
|
|
75
|
+
- inject only the full original project prompt into the `{prompt}` placeholder and leave the rest of the template body unchanged
|
|
76
|
+
- send that resulting evaluation prompt text verbatim to one fresh `General` evaluator session with zero additions or reductions beyond the `{prompt}` substitution
|
|
77
|
+
- let the prompt itself define the evaluator output contract; do not append extra response requirements outside the prompt body
|
|
84
78
|
- assign the next audit number
|
|
85
79
|
- if and only if the verdict is `partial pass`, keep the normalized report path as `../.tmp/audit_report-<N>.md`
|
|
86
|
-
- if the verdict is `fail` or `pass`, discard the generated report file after extracting the
|
|
80
|
+
- if the verdict is `fail` or `pass`, discard the generated report file after extracting the verdict or issue list you need from the evaluator result and/or report contents
|
|
87
81
|
- record the evaluator session id, prompt kind, audit number, verdict, kept-or-discarded report status, and routing decision in metadata
|
|
88
82
|
|
|
89
83
|
## Fresh-audit branching rule
|
|
@@ -123,17 +117,16 @@ Inside a `partial pass` audit's bugfix loop:
|
|
|
123
117
|
- after the developer claims the fixes are done, run a rough targeted owner-side verification pass on the affected behavior before asking for evaluator confirmation
|
|
124
118
|
- then return to the same evaluator session and send only the exact issue list or current unresolved subset for scoped fix confirmation
|
|
125
119
|
- require a file-backed fix-check report for that scoped verification pass
|
|
126
|
-
- store
|
|
127
|
-
- if unresolved issues remain, take only that unresolved subset back to the same bugfix session and repeat the same-session fix-check loop
|
|
120
|
+
- store the scoped fix-check report as `../.tmp/audit_report-<N>-fix_check.md`
|
|
121
|
+
- if unresolved issues remain, take only that unresolved subset back to the same bugfix session and repeat the same-session fix-check loop, replacing the same fix-check report each time so it always covers the whole audit issue list in its latest state
|
|
128
122
|
- once all issues from `audit_report-<N>.md` are resolved, mark that bugfix session completed in metadata
|
|
129
123
|
|
|
130
124
|
## Post-bugfix coverage and README audit
|
|
131
125
|
|
|
132
126
|
- after 2 bugfix sessions have been completed, do not leave `P7` yet; this audit is the last subphase inside `P7`
|
|
133
|
-
- read `~/slopmachine/test-coverage-prompt.md` yourself before launching the audit
|
|
134
127
|
- launch a fresh `General` evaluator session for this audit
|
|
135
128
|
- prepare the audit workspace with `node ~/slopmachine/utils/prepare_strict_audit_workspace.mjs --workspace-root .. --name test-coverage-readme-audit` and use the returned `run_dir` as the evaluator working directory so `repo/README.md` and `../.tmp/` both resolve correctly
|
|
136
|
-
-
|
|
129
|
+
- send `~/slopmachine/test-coverage-prompt.md` verbatim with zero additions or reductions; do not prepend cwd notes, workflow notes, or custom audit instructions because that prompt already defines its own report path and audit workspace assumptions
|
|
137
130
|
- before each rerun, remove or replace the previous `../.tmp/test_coverage_and_readme_audit_report.md`; do not keep numbered variants for this report
|
|
138
131
|
- if the report finds any issue, treat that as blocking `P7` completion
|
|
139
132
|
- route those issues to the currently active recoverable developer session; prefer the most recently used developer session, which will usually be `bugfix-2`
|
|
@@ -155,10 +148,10 @@ Inside a `partial pass` audit's bugfix loop:
|
|
|
155
148
|
- `P7` is complete only after 2 bugfix sessions have been completed and the post-bugfix coverage/README audit has run as the last subphase of `P7`
|
|
156
149
|
- the second bugfix session must be completed by resolving its scoped issue list through the same-audit fix-check loop
|
|
157
150
|
- fresh `pass` audits before that point are discarded clean audits and do not replace the 2-bugfix-session requirement
|
|
158
|
-
- after the second bugfix session completes, run the coverage/README audit; if it becomes clean within 3 remediation attempts, move to `P8 Final
|
|
151
|
+
- after the second bugfix session completes, run the coverage/README audit; if it becomes clean within 3 remediation attempts, move to `P8 Final Readiness Decision` with a clean report, otherwise move to `P8 Final Readiness Decision` with the latest final report after the third attempt
|
|
159
152
|
|
|
160
153
|
## Boundaries
|
|
161
154
|
|
|
162
|
-
- this phase is owner-side evaluation orchestration, not
|
|
155
|
+
- this phase is owner-side evaluation orchestration, not a user approval gate
|
|
163
156
|
- keep audit numbering deterministic and monotonic across the whole run
|
|
164
157
|
- do not reopen the old counted-cycle report-root model
|
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
16
|
-
-
|
|
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
|
|
|
@@ -163,7 +163,7 @@ Any earlier extra Docker run needs a concrete blocker-based justification.
|
|
|
163
163
|
|
|
164
164
|
Use evidence such as internal metadata files, structured Beads comments, verification command results, and file/project-state checks.
|
|
165
165
|
|
|
166
|
-
- clarification requires the `clarification-gate` conditions plus
|
|
166
|
+
- clarification requires the `clarification-gate` conditions plus an internally accepted clarification record that is ready to roll directly into planning
|
|
167
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
|
|
168
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
|
|
169
169
|
- planning exit does not pass if those sections exist only nominally or remain too vague to drive implementation without broad reinvention
|
|
@@ -214,7 +214,7 @@ Use evidence such as internal metadata files, structured Beads comments, verific
|
|
|
214
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
|
|
215
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
|
|
216
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
|
|
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
|
|
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
|
|
218
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`
|
|
219
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
|
|
220
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
|
|
@@ -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
|
|
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
|
|
@@ -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
|
|
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
|
|
Binary file
|
|
@@ -8,9 +8,9 @@ import crypto from 'node:crypto'
|
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import { spawn } from 'node:child_process'
|
|
10
10
|
|
|
11
|
-
import { emitFailure, emitSuccess, extractRateLimitMetadata, parseArgs, readJsonFile, readPrompt, sleep, waitForRateLimitReset, writeFileIfNeeded, writeJsonIfNeeded } from './claude_worker_common.mjs'
|
|
11
|
+
import { emitFailure, emitSuccess, extractRateLimitMetadata, parseArgs, readJsonFile, readPrompt, readPromptInput, sleep, waitForRateLimitReset, writeFileIfNeeded, writeJsonIfNeeded } from './claude_worker_common.mjs'
|
|
12
12
|
|
|
13
|
-
export { emitFailure, emitSuccess, parseArgs, readPrompt, sleep, waitForRateLimitReset, writeJsonIfNeeded }
|
|
13
|
+
export { emitFailure, emitSuccess, parseArgs, readPrompt, readPromptInput, sleep, waitForRateLimitReset, writeJsonIfNeeded }
|
|
14
14
|
|
|
15
15
|
export const DEFAULT_LAUNCH_TIMEOUT_MS = 3600000
|
|
16
16
|
export const DEFAULT_TURN_TIMEOUT_MS = 3600000
|
|
@@ -170,17 +170,56 @@ export async function waitFor(predicate, { timeoutMs, intervalMs = DEFAULT_POLL_
|
|
|
170
170
|
throw new Error(errorMessage)
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
function detectClaudeStartupPrompt(pane) {
|
|
174
|
+
if (!pane) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
pane.includes('Quick safety check:')
|
|
180
|
+
|| pane.includes('Yes, I trust this folder')
|
|
181
|
+
|| pane.includes('Accessing workspace:')
|
|
182
|
+
) {
|
|
183
|
+
return 'workspace-trust'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
pane.includes('WARNING: Loading development channels')
|
|
188
|
+
|| pane.includes('I am using this for local development')
|
|
189
|
+
) {
|
|
190
|
+
return 'development-channels'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function maybeAcceptClaudeStartupPrompts(sessionName, timeoutMs = 30000) {
|
|
174
197
|
const deadline = Date.now() + timeoutMs
|
|
198
|
+
let lastAcceptedPrompt = null
|
|
199
|
+
let lastAcceptedAt = 0
|
|
200
|
+
|
|
175
201
|
while (Date.now() < deadline) {
|
|
176
202
|
const pane = await tmuxCapturePane(sessionName)
|
|
177
203
|
if (pane.includes('Listening for channel messages from:')) {
|
|
178
204
|
return false
|
|
179
205
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
|
|
207
|
+
const promptKind = detectClaudeStartupPrompt(pane)
|
|
208
|
+
if (promptKind) {
|
|
209
|
+
const now = Date.now()
|
|
210
|
+
if (promptKind !== lastAcceptedPrompt || now - lastAcceptedAt >= 1500) {
|
|
211
|
+
lastAcceptedPrompt = promptKind
|
|
212
|
+
lastAcceptedAt = now
|
|
213
|
+
await tmuxSendEnter(sessionName)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await sleep(DEFAULT_POLL_INTERVAL_MS)
|
|
217
|
+
continue
|
|
183
218
|
}
|
|
219
|
+
|
|
220
|
+
lastAcceptedPrompt = null
|
|
221
|
+
lastAcceptedAt = 0
|
|
222
|
+
|
|
184
223
|
await sleep(DEFAULT_POLL_INTERVAL_MS)
|
|
185
224
|
}
|
|
186
225
|
return false
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ensureRuntimeDirs,
|
|
16
16
|
makeSuffix,
|
|
17
17
|
makeToken,
|
|
18
|
-
|
|
18
|
+
maybeAcceptClaudeStartupPrompts,
|
|
19
19
|
parseArgs,
|
|
20
20
|
readJsonIfExists,
|
|
21
21
|
resolveUtilsDir,
|
|
@@ -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 || '
|
|
40
|
-
const laneEffort = argv.effort ||
|
|
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'
|
|
@@ -147,7 +147,7 @@ try {
|
|
|
147
147
|
process.exit(1)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
await
|
|
150
|
+
await maybeAcceptClaudeStartupPrompts(tmuxSession, launchTimeoutMs)
|
|
151
151
|
|
|
152
152
|
const [{ event }, channelState] = await Promise.all([
|
|
153
153
|
waitForHookEvent(paths, 0, new Set(['SessionStart']), launchTimeoutMs),
|
|
@@ -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
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
|