@kynetic-ai/spec 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -17
- package/dist/acp/client.d.ts +18 -4
- package/dist/acp/client.d.ts.map +1 -1
- package/dist/acp/client.js +44 -26
- package/dist/acp/client.js.map +1 -1
- package/dist/acp/framing.d.ts +2 -2
- package/dist/acp/framing.d.ts.map +1 -1
- package/dist/acp/framing.js +37 -29
- package/dist/acp/framing.js.map +1 -1
- package/dist/acp/index.d.ts +6 -7
- package/dist/acp/index.d.ts.map +1 -1
- package/dist/acp/index.js +3 -3
- package/dist/acp/index.js.map +1 -1
- package/dist/acp/types.d.ts +5 -5
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js +18 -18
- package/dist/acp/types.js.map +1 -1
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +24 -13
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/index.d.ts +2 -2
- package/dist/agents/index.js +2 -2
- package/dist/agents/spawner.d.ts +4 -4
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +6 -6
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/batch-context.d.ts +43 -0
- package/dist/cli/batch-context.d.ts.map +1 -0
- package/dist/cli/batch-context.js +93 -0
- package/dist/cli/batch-context.js.map +1 -0
- package/dist/cli/batch-exec.d.ts +116 -0
- package/dist/cli/batch-exec.d.ts.map +1 -0
- package/dist/cli/batch-exec.js +694 -0
- package/dist/cli/batch-exec.js.map +1 -0
- package/dist/cli/batch.d.ts +4 -2
- package/dist/cli/batch.d.ts.map +1 -1
- package/dist/cli/batch.js +15 -14
- package/dist/cli/batch.js.map +1 -1
- package/dist/cli/command-annotations.d.ts +23 -0
- package/dist/cli/command-annotations.d.ts.map +1 -0
- package/dist/cli/command-annotations.js +27 -0
- package/dist/cli/command-annotations.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +46 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +377 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/batch.d.ts +20 -0
- package/dist/cli/commands/batch.d.ts.map +1 -0
- package/dist/cli/commands/batch.js +214 -0
- package/dist/cli/commands/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +1 -1
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
- package/dist/cli/commands/clone-for-testing.js +37 -47
- package/dist/cli/commands/clone-for-testing.js.map +1 -1
- package/dist/cli/commands/derive.d.ts +1 -1
- package/dist/cli/commands/derive.d.ts.map +1 -1
- package/dist/cli/commands/derive.js +140 -88
- package/dist/cli/commands/derive.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +11 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +152 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts +12 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +134 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/help.d.ts +1 -1
- package/dist/cli/commands/help.d.ts.map +1 -1
- package/dist/cli/commands/help.js +163 -37
- package/dist/cli/commands/help.js.map +1 -1
- package/dist/cli/commands/inbox.d.ts +1 -1
- package/dist/cli/commands/inbox.d.ts.map +1 -1
- package/dist/cli/commands/inbox.js +178 -56
- package/dist/cli/commands/inbox.js.map +1 -1
- package/dist/cli/commands/index.d.ts +31 -19
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +31 -19
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +108 -57
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/item.d.ts +1 -1
- package/dist/cli/commands/item.d.ts.map +1 -1
- package/dist/cli/commands/item.js +557 -274
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/link.d.ts +1 -1
- package/dist/cli/commands/link.d.ts.map +1 -1
- package/dist/cli/commands/link.js +55 -46
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/log.d.ts +1 -1
- package/dist/cli/commands/log.d.ts.map +1 -1
- package/dist/cli/commands/log.js +57 -51
- package/dist/cli/commands/log.js.map +1 -1
- package/dist/cli/commands/merge-driver.d.ts +19 -0
- package/dist/cli/commands/merge-driver.d.ts.map +1 -0
- package/dist/cli/commands/merge-driver.js +398 -0
- package/dist/cli/commands/merge-driver.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +1 -1
- package/dist/cli/commands/meta.d.ts.map +1 -1
- package/dist/cli/commands/meta.js +533 -399
- package/dist/cli/commands/meta.js.map +1 -1
- package/dist/cli/commands/module.d.ts +1 -1
- package/dist/cli/commands/module.d.ts.map +1 -1
- package/dist/cli/commands/module.js +30 -25
- package/dist/cli/commands/module.js.map +1 -1
- package/dist/cli/commands/plan-import.d.ts +11 -0
- package/dist/cli/commands/plan-import.d.ts.map +1 -0
- package/dist/cli/commands/plan-import.js +516 -0
- package/dist/cli/commands/plan-import.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +10 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +421 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +1 -1
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +1100 -168
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/refs.d.ts +13 -0
- package/dist/cli/commands/refs.d.ts.map +1 -0
- package/dist/cli/commands/refs.js +283 -0
- package/dist/cli/commands/refs.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +199 -37
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +10 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +491 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/session.d.ts +25 -6
- package/dist/cli/commands/session.d.ts.map +1 -1
- package/dist/cli/commands/session.js +811 -127
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +81 -0
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
- package/dist/cli/commands/setup-seeding.js +292 -0
- package/dist/cli/commands/setup-seeding.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +77 -3
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +1233 -274
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/shadow.d.ts +1 -1
- package/dist/cli/commands/shadow.d.ts.map +1 -1
- package/dist/cli/commands/shadow.js +70 -50
- package/dist/cli/commands/shadow.js.map +1 -1
- package/dist/cli/commands/skill-crud.d.ts +58 -0
- package/dist/cli/commands/skill-crud.d.ts.map +1 -0
- package/dist/cli/commands/skill-crud.js +753 -0
- package/dist/cli/commands/skill-crud.js.map +1 -0
- package/dist/cli/commands/skill-diff.d.ts +27 -0
- package/dist/cli/commands/skill-diff.d.ts.map +1 -0
- package/dist/cli/commands/skill-diff.js +840 -0
- package/dist/cli/commands/skill-diff.js.map +1 -0
- package/dist/cli/commands/skill-install.d.ts +53 -0
- package/dist/cli/commands/skill-install.d.ts.map +1 -0
- package/dist/cli/commands/skill-install.js +452 -0
- package/dist/cli/commands/skill-install.js.map +1 -0
- package/dist/cli/commands/skill.d.ts +20 -0
- package/dist/cli/commands/skill.d.ts.map +1 -0
- package/dist/cli/commands/skill.js +36 -0
- package/dist/cli/commands/skill.js.map +1 -0
- package/dist/cli/commands/task.d.ts +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +569 -346
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/tasks.d.ts +26 -1
- package/dist/cli/commands/tasks.d.ts.map +1 -1
- package/dist/cli/commands/tasks.js +227 -122
- package/dist/cli/commands/tasks.js.map +1 -1
- package/dist/cli/commands/trait.d.ts +1 -1
- package/dist/cli/commands/trait.d.ts.map +1 -1
- package/dist/cli/commands/trait.js +166 -101
- package/dist/cli/commands/trait.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +569 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/commands/util.d.ts +7 -0
- package/dist/cli/commands/util.d.ts.map +1 -0
- package/dist/cli/commands/util.js +30 -0
- package/dist/cli/commands/util.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +264 -83
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +851 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +26 -18
- package/dist/cli/exit-codes.js.map +1 -1
- package/dist/cli/help/content.d.ts.map +1 -1
- package/dist/cli/help/content.js +86 -71
- package/dist/cli/help/content.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +135 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/introspection.d.ts +6 -2
- package/dist/cli/introspection.d.ts.map +1 -1
- package/dist/cli/introspection.js +11 -8
- package/dist/cli/introspection.js.map +1 -1
- package/dist/cli/output.d.ts +64 -4
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +235 -85
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/parse-utils.d.ts +21 -0
- package/dist/cli/parse-utils.d.ts.map +1 -0
- package/dist/cli/parse-utils.js +32 -0
- package/dist/cli/parse-utils.js.map +1 -0
- package/dist/cli/pid-utils.d.ts +72 -0
- package/dist/cli/pid-utils.d.ts.map +1 -0
- package/dist/cli/pid-utils.js +174 -0
- package/dist/cli/pid-utils.js.map +1 -0
- package/dist/cli/suggest.d.ts.map +1 -1
- package/dist/cli/suggest.js +1 -2
- package/dist/cli/suggest.js.map +1 -1
- package/dist/cli/validators.d.ts +43 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +84 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/daemon/index.ts +52 -0
- package/dist/daemon/middleware/project-context.ts +126 -0
- package/dist/daemon/pid.ts +179 -0
- package/dist/daemon/project-context.ts +343 -0
- package/dist/daemon/routes/inbox.ts +164 -0
- package/dist/daemon/routes/items.ts +322 -0
- package/dist/daemon/routes/meta.ts +118 -0
- package/dist/daemon/routes/projects.ts +162 -0
- package/dist/daemon/routes/tasks.ts +327 -0
- package/dist/daemon/routes/triage.ts +468 -0
- package/dist/daemon/routes/validation.ts +248 -0
- package/dist/daemon/server.ts +408 -0
- package/dist/daemon/watcher.ts +195 -0
- package/dist/daemon/websocket/handler.ts +138 -0
- package/dist/daemon/websocket/heartbeat.ts +71 -0
- package/dist/daemon/websocket/pubsub.ts +125 -0
- package/dist/daemon/websocket/types.ts +66 -0
- package/dist/export/html.d.ts +19 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +239 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/index.d.ts +10 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +10 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/json.d.ts +24 -0
- package/dist/export/json.d.ts.map +1 -0
- package/dist/export/json.js +198 -0
- package/dist/export/json.js.map +1 -0
- package/dist/export/triage.d.ts +51 -0
- package/dist/export/triage.d.ts.map +1 -0
- package/dist/export/triage.js +83 -0
- package/dist/export/triage.js.map +1 -0
- package/dist/export/types.d.ts +122 -0
- package/dist/export/types.d.ts.map +1 -0
- package/dist/export/types.js +9 -0
- package/dist/export/types.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/claude-plugin-registry.d.ts +66 -0
- package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
- package/dist/lib/claude-plugin-registry.js +318 -0
- package/dist/lib/claude-plugin-registry.js.map +1 -0
- package/dist/merge/arrays.d.ts +87 -0
- package/dist/merge/arrays.d.ts.map +1 -0
- package/dist/merge/arrays.js +164 -0
- package/dist/merge/arrays.js.map +1 -0
- package/dist/merge/file-type.d.ts +32 -0
- package/dist/merge/file-type.d.ts.map +1 -0
- package/dist/merge/file-type.js +70 -0
- package/dist/merge/file-type.js.map +1 -0
- package/dist/merge/index.d.ts +14 -0
- package/dist/merge/index.d.ts.map +1 -0
- package/dist/merge/index.js +11 -0
- package/dist/merge/index.js.map +1 -0
- package/dist/merge/objects.d.ts +46 -0
- package/dist/merge/objects.d.ts.map +1 -0
- package/dist/merge/objects.js +193 -0
- package/dist/merge/objects.js.map +1 -0
- package/dist/merge/parse.d.ts +23 -0
- package/dist/merge/parse.d.ts.map +1 -0
- package/dist/merge/parse.js +78 -0
- package/dist/merge/parse.js.map +1 -0
- package/dist/merge/resolve.d.ts +66 -0
- package/dist/merge/resolve.d.ts.map +1 -0
- package/dist/merge/resolve.js +189 -0
- package/dist/merge/resolve.js.map +1 -0
- package/dist/merge/types.d.ts +82 -0
- package/dist/merge/types.d.ts.map +1 -0
- package/dist/merge/types.js +8 -0
- package/dist/merge/types.js.map +1 -0
- package/dist/parser/agent-data-sections.d.ts +53 -0
- package/dist/parser/agent-data-sections.d.ts.map +1 -0
- package/dist/parser/agent-data-sections.js +118 -0
- package/dist/parser/agent-data-sections.js.map +1 -0
- package/dist/parser/alignment.d.ts +4 -4
- package/dist/parser/alignment.d.ts.map +1 -1
- package/dist/parser/alignment.js +27 -22
- package/dist/parser/alignment.js.map +1 -1
- package/dist/parser/assess.d.ts +5 -5
- package/dist/parser/assess.d.ts.map +1 -1
- package/dist/parser/assess.js +36 -32
- package/dist/parser/assess.js.map +1 -1
- package/dist/parser/config.d.ts +351 -0
- package/dist/parser/config.d.ts.map +1 -0
- package/dist/parser/config.js +326 -0
- package/dist/parser/config.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +1 -1
- package/dist/parser/convention-validation.d.ts.map +1 -1
- package/dist/parser/convention-validation.js +21 -16
- package/dist/parser/convention-validation.js.map +1 -1
- package/dist/parser/coverage-cache.d.ts +49 -0
- package/dist/parser/coverage-cache.d.ts.map +1 -0
- package/dist/parser/coverage-cache.js +123 -0
- package/dist/parser/coverage-cache.js.map +1 -0
- package/dist/parser/daemon-status.d.ts +37 -0
- package/dist/parser/daemon-status.d.ts.map +1 -0
- package/dist/parser/daemon-status.js +67 -0
- package/dist/parser/daemon-status.js.map +1 -0
- package/dist/parser/doctor.d.ts +107 -0
- package/dist/parser/doctor.d.ts.map +1 -0
- package/dist/parser/doctor.js +366 -0
- package/dist/parser/doctor.js.map +1 -0
- package/dist/parser/fix.d.ts +1 -1
- package/dist/parser/fix.d.ts.map +1 -1
- package/dist/parser/fix.js +31 -27
- package/dist/parser/fix.js.map +1 -1
- package/dist/parser/index.d.ts +16 -11
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +16 -11
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/items.d.ts +8 -2
- package/dist/parser/items.d.ts.map +1 -1
- package/dist/parser/items.js +71 -35
- package/dist/parser/items.js.map +1 -1
- package/dist/parser/meta.d.ts +167 -9
- package/dist/parser/meta.d.ts.map +1 -1
- package/dist/parser/meta.js +379 -46
- package/dist/parser/meta.js.map +1 -1
- package/dist/parser/plan-document.d.ts +189 -0
- package/dist/parser/plan-document.d.ts.map +1 -0
- package/dist/parser/plan-document.js +340 -0
- package/dist/parser/plan-document.js.map +1 -0
- package/dist/parser/plans.d.ts +59 -0
- package/dist/parser/plans.d.ts.map +1 -0
- package/dist/parser/plans.js +239 -0
- package/dist/parser/plans.js.map +1 -0
- package/dist/parser/refs.d.ts +22 -9
- package/dist/parser/refs.d.ts.map +1 -1
- package/dist/parser/refs.js +102 -50
- package/dist/parser/refs.js.map +1 -1
- package/dist/parser/setup-status.d.ts +71 -0
- package/dist/parser/setup-status.d.ts.map +1 -0
- package/dist/parser/setup-status.js +269 -0
- package/dist/parser/setup-status.js.map +1 -0
- package/dist/parser/shadow.d.ts +150 -19
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +548 -187
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +317 -0
- package/dist/parser/skill-render.d.ts.map +1 -0
- package/dist/parser/skill-render.js +943 -0
- package/dist/parser/skill-render.js.map +1 -0
- package/dist/parser/traits.d.ts +3 -3
- package/dist/parser/traits.d.ts.map +1 -1
- package/dist/parser/traits.js +2 -2
- package/dist/parser/traits.js.map +1 -1
- package/dist/parser/validate-skills.d.ts +32 -0
- package/dist/parser/validate-skills.d.ts.map +1 -0
- package/dist/parser/validate-skills.js +202 -0
- package/dist/parser/validate-skills.js.map +1 -0
- package/dist/parser/validate.d.ts +45 -3
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +622 -105
- package/dist/parser/validate.js.map +1 -1
- package/dist/parser/yaml.d.ts +83 -19
- package/dist/parser/yaml.d.ts.map +1 -1
- package/dist/parser/yaml.js +478 -173
- package/dist/parser/yaml.js.map +1 -1
- package/dist/ralph/cli-renderer.d.ts +8 -1
- package/dist/ralph/cli-renderer.d.ts.map +1 -1
- package/dist/ralph/cli-renderer.js +105 -34
- package/dist/ralph/cli-renderer.js.map +1 -1
- package/dist/ralph/events.d.ts +10 -10
- package/dist/ralph/events.d.ts.map +1 -1
- package/dist/ralph/events.js +277 -98
- package/dist/ralph/events.js.map +1 -1
- package/dist/ralph/index.d.ts +5 -2
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +9 -3
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/loop-errors.d.ts +83 -0
- package/dist/ralph/loop-errors.d.ts.map +1 -0
- package/dist/ralph/loop-errors.js +150 -0
- package/dist/ralph/loop-errors.js.map +1 -0
- package/dist/ralph/subagent.d.ts +83 -0
- package/dist/ralph/subagent.d.ts.map +1 -0
- package/dist/ralph/subagent.js +174 -0
- package/dist/ralph/subagent.js.map +1 -0
- package/dist/ralph/wrap-up.d.ts +125 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -0
- package/dist/ralph/wrap-up.js +270 -0
- package/dist/ralph/wrap-up.js.map +1 -0
- package/dist/schema/batch.d.ts +95 -0
- package/dist/schema/batch.d.ts.map +1 -0
- package/dist/schema/batch.js +24 -0
- package/dist/schema/batch.js.map +1 -0
- package/dist/schema/common.d.ts +2 -2
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +34 -31
- package/dist/schema/common.js.map +1 -1
- package/dist/schema/inbox.d.ts +12 -12
- package/dist/schema/inbox.js +4 -4
- package/dist/schema/inbox.js.map +1 -1
- package/dist/schema/index.d.ts +8 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +8 -5
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/meta.d.ts +1454 -27
- package/dist/schema/meta.d.ts.map +1 -1
- package/dist/schema/meta.js +198 -21
- package/dist/schema/meta.js.map +1 -1
- package/dist/schema/plan.d.ts +285 -0
- package/dist/schema/plan.d.ts.map +1 -0
- package/dist/schema/plan.js +81 -0
- package/dist/schema/plan.js.map +1 -0
- package/dist/schema/spec.d.ts +72 -33
- package/dist/schema/spec.d.ts.map +1 -1
- package/dist/schema/spec.js +22 -9
- package/dist/schema/spec.js.map +1 -1
- package/dist/schema/task.d.ts +172 -161
- package/dist/schema/task.d.ts.map +1 -1
- package/dist/schema/task.js +21 -12
- package/dist/schema/task.js.map +1 -1
- package/dist/schema/triage.d.ts +266 -0
- package/dist/schema/triage.d.ts.map +1 -0
- package/dist/schema/triage.js +134 -0
- package/dist/schema/triage.js.map +1 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +3 -3
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/store.d.ts +233 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +628 -31
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +10 -10
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js +10 -9
- package/dist/sessions/types.js.map +1 -1
- package/dist/strings/errors.d.ts +51 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +136 -106
- package/dist/strings/errors.js.map +1 -1
- package/dist/strings/guidance.d.ts.map +1 -1
- package/dist/strings/guidance.js +16 -16
- package/dist/strings/guidance.js.map +1 -1
- package/dist/strings/index.d.ts +4 -4
- package/dist/strings/index.d.ts.map +1 -1
- package/dist/strings/index.js +4 -4
- package/dist/strings/index.js.map +1 -1
- package/dist/strings/labels.d.ts +4 -0
- package/dist/strings/labels.d.ts.map +1 -1
- package/dist/strings/labels.js +45 -41
- package/dist/strings/labels.js.map +1 -1
- package/dist/strings/validation.d.ts.map +1 -1
- package/dist/strings/validation.js +71 -71
- package/dist/strings/validation.js.map +1 -1
- package/dist/utils/commit.d.ts +1 -1
- package/dist/utils/commit.d.ts.map +1 -1
- package/dist/utils/commit.js +28 -26
- package/dist/utils/commit.js.map +1 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +40 -38
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/grep.js +11 -11
- package/dist/utils/grep.js.map +1 -1
- package/dist/utils/index.d.ts +7 -7
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +10 -10
- package/dist/utils/time.js.map +1 -1
- package/package.json +28 -5
- package/plugin/.claude-plugin/marketplace.json +17 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
- package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
- package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
- package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
- package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
- package/templates/agents-sections/01-quick-start.md +22 -0
- package/templates/agents-sections/02-shadow-branch.md +34 -0
- package/templates/agents-sections/03-task-lifecycle.md +48 -0
- package/templates/agents-sections/04-pr-workflow.md +17 -0
- package/templates/agents-sections/05-commit-convention.md +27 -0
- package/templates/agents-sections/06-ralph-loop.md +45 -0
- package/templates/hooks/pre-commit +34 -0
- package/templates/skills/help/SKILL.md +37 -0
- package/templates/skills/manifest.yaml +15 -0
- package/templates/skills/triage/SKILL.md +199 -0
- package/templates/skills/triage/docs/automation.md +120 -0
- package/templates/skills/triage/docs/inbox.md +144 -0
- package/templates/skills/triage/docs/observations.md +85 -0
|
@@ -4,104 +4,348 @@
|
|
|
4
4
|
* Runs an ACP-compliant agent in a loop to process tasks autonomously.
|
|
5
5
|
* Uses session event storage for full audit trail and streaming output.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import * as path from
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { ulid } from "ulid";
|
|
13
|
+
// Read version from package.json for ACP client info
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { version: packageVersion } = require("../../../package.json");
|
|
16
|
+
import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
|
|
17
|
+
import { spawnAndInitialize } from "../../agents/spawner.js";
|
|
18
|
+
import { initContext, loadAllItems, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
|
|
19
|
+
import { buildWrapUpContext, createCliRenderer, createTranslator, DEFAULT_SUBAGENT_PREFIX, DEFAULT_WRAPUP_TIMEOUT, RALPH_PROMPT_TIMEOUT, runSubagent, runWrapUpAgent, WRAPUP_AGENT_PREFIX, } from "../../ralph/index.js";
|
|
20
|
+
import { appendEvent, createSession, saveSessionContext, updateSessionStatus, } from "../../sessions/index.js";
|
|
21
|
+
import { errors } from "../../strings/index.js";
|
|
22
|
+
import { getCurrentBranch } from "../../utils/git.js";
|
|
23
|
+
import { EXIT_CODES } from "../exit-codes.js";
|
|
24
|
+
import { error, info, success, warn } from "../output.js";
|
|
25
|
+
import { gatherSessionContext, getIterationStats, } from "./session.js";
|
|
26
|
+
const TASK_LIMIT_MARKER_PATH = ".claude/ralph-task-limit.json";
|
|
27
|
+
const END_LOOP_MARKER_PATH = ".claude/ralph-end-loop.json";
|
|
28
|
+
const STALE_MARKER_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
|
|
29
|
+
/**
|
|
30
|
+
* Write task limit marker file.
|
|
31
|
+
* AC: @ralph-task-limit ac-wrapup, ac-marker-format
|
|
32
|
+
*/
|
|
33
|
+
async function writeTaskLimitMarker(rootDir, marker) {
|
|
34
|
+
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
35
|
+
const dir = path.dirname(markerPath);
|
|
36
|
+
await fs.mkdir(dir, { recursive: true });
|
|
37
|
+
await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read task limit marker file if it exists.
|
|
41
|
+
*/
|
|
42
|
+
async function readTaskLimitMarker(rootDir) {
|
|
43
|
+
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
44
|
+
try {
|
|
45
|
+
const content = await fs.readFile(markerPath, "utf-8");
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear task limit marker file.
|
|
54
|
+
* AC: @ralph-task-limit ac-reset
|
|
55
|
+
*/
|
|
56
|
+
async function clearTaskLimitMarker(rootDir) {
|
|
57
|
+
const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
|
|
58
|
+
try {
|
|
59
|
+
await fs.unlink(markerPath);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Ignore if file doesn't exist
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clear stale marker files (older than 1 hour).
|
|
67
|
+
* AC: @ralph-task-limit ac-reset
|
|
68
|
+
*/
|
|
69
|
+
async function clearStaleMarker(rootDir) {
|
|
70
|
+
const marker = await readTaskLimitMarker(rootDir);
|
|
71
|
+
if (!marker)
|
|
72
|
+
return false;
|
|
73
|
+
const markerAge = Date.now() - new Date(marker.since).getTime();
|
|
74
|
+
if (markerAge > STALE_MARKER_THRESHOLD_MS) {
|
|
75
|
+
await clearTaskLimitMarker(rootDir);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Write end-loop marker file.
|
|
82
|
+
* AC: @ralph-end-loop ac-cmd
|
|
83
|
+
*/
|
|
84
|
+
async function writeEndLoopMarker(rootDir, reason) {
|
|
85
|
+
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
86
|
+
const dir = path.dirname(markerPath);
|
|
87
|
+
await fs.mkdir(dir, { recursive: true });
|
|
88
|
+
const marker = {
|
|
89
|
+
requested: true,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
reason,
|
|
92
|
+
};
|
|
93
|
+
await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Read end-loop marker file if it exists.
|
|
97
|
+
* AC: @ralph-end-loop ac-detect
|
|
98
|
+
*/
|
|
99
|
+
async function readEndLoopMarker(rootDir) {
|
|
100
|
+
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
101
|
+
try {
|
|
102
|
+
const content = await fs.readFile(markerPath, "utf-8");
|
|
103
|
+
return JSON.parse(content);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Clear end-loop marker file.
|
|
111
|
+
* AC: @ralph-end-loop ac-cleanup
|
|
112
|
+
*/
|
|
113
|
+
async function clearEndLoopMarker(rootDir) {
|
|
114
|
+
const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
|
|
115
|
+
try {
|
|
116
|
+
await fs.unlink(markerPath);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore if file doesn't exist
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Clear stale end-loop markers (older than 1 hour).
|
|
124
|
+
* AC: @ralph-end-loop ac-cleanup
|
|
125
|
+
*/
|
|
126
|
+
async function clearStaleEndLoopMarker(rootDir) {
|
|
127
|
+
const marker = await readEndLoopMarker(rootDir);
|
|
128
|
+
if (!marker)
|
|
129
|
+
return false;
|
|
130
|
+
const markerAge = Date.now() - new Date(marker.timestamp).getTime();
|
|
131
|
+
if (markerAge > STALE_MARKER_THRESHOLD_MS) {
|
|
132
|
+
await clearEndLoopMarker(rootDir);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Detect if a Bash command is a task complete command.
|
|
139
|
+
* AC: @ralph-task-limit ac-detection
|
|
140
|
+
*/
|
|
141
|
+
function detectTaskCompleteCommand(command) {
|
|
142
|
+
// Match variations of "kspec task complete"
|
|
143
|
+
// Don't match "kspec task submit" - that's just status change to pending_review
|
|
144
|
+
return /\bkspec\s+task\s+complete\b/.test(command);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Detect if a Bash command is an end-loop command.
|
|
148
|
+
* AC: @ralph-end-loop ac-detect
|
|
149
|
+
*/
|
|
150
|
+
function detectEndLoopCommand(command) {
|
|
151
|
+
// Match "kspec ralph end-loop" with any arguments
|
|
152
|
+
return /\bkspec\s+ralph\s+end-loop\b/.test(command);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract Bash command from SessionUpdate if it's a tool_call or tool_call_update event.
|
|
156
|
+
* Returns null if not a Bash tool call.
|
|
157
|
+
*/
|
|
158
|
+
function extractBashCommand(update) {
|
|
159
|
+
const u = update;
|
|
160
|
+
// Check if this is a tool call event
|
|
161
|
+
if (u.sessionUpdate !== "tool_call" && u.sessionUpdate !== "tool_call_update") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// Extract tool name - check various locations Claude Code uses
|
|
165
|
+
let toolName;
|
|
166
|
+
// Try _meta.claudeCode.toolName first (Claude Code pattern)
|
|
167
|
+
const meta = u._meta;
|
|
168
|
+
if (meta) {
|
|
169
|
+
const claudeCode = meta.claudeCode;
|
|
170
|
+
if (claudeCode?.toolName) {
|
|
171
|
+
toolName = String(claudeCode.toolName);
|
|
172
|
+
}
|
|
173
|
+
else if (meta.toolName) {
|
|
174
|
+
toolName = String(meta.toolName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Fall back to name or title field
|
|
178
|
+
if (!toolName && u.name) {
|
|
179
|
+
toolName = String(u.name);
|
|
180
|
+
}
|
|
181
|
+
if (!toolName && u.title) {
|
|
182
|
+
toolName = String(u.title);
|
|
183
|
+
}
|
|
184
|
+
// Check if it's a Bash tool (handle MCP prefix variations)
|
|
185
|
+
if (!toolName)
|
|
186
|
+
return null;
|
|
187
|
+
const isBash = toolName === "Bash" || toolName.endsWith("__Bash");
|
|
188
|
+
if (!isBash)
|
|
189
|
+
return null;
|
|
190
|
+
// Extract command from input
|
|
191
|
+
const input = (u.rawInput || u.input || u.params);
|
|
192
|
+
if (!input)
|
|
193
|
+
return null;
|
|
194
|
+
const command = input.command;
|
|
195
|
+
if (typeof command !== "string")
|
|
196
|
+
return null;
|
|
197
|
+
return command;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Parse and validate --tasks flag value.
|
|
201
|
+
* Returns resolved ULIDs for the specified task refs.
|
|
202
|
+
* AC: @cli-ralph ac-21
|
|
203
|
+
*
|
|
204
|
+
* @throws Error if any ref cannot be resolved or is not a task
|
|
205
|
+
*/
|
|
206
|
+
async function parseExplicitTasks(ctx, tasksArg) {
|
|
207
|
+
const refs = tasksArg.split(",").map((r) => r.trim()).filter(Boolean);
|
|
208
|
+
if (refs.length === 0) {
|
|
209
|
+
throw new Error("--tasks requires at least one task reference");
|
|
210
|
+
}
|
|
211
|
+
// Load tasks and items for resolution
|
|
212
|
+
const tasks = await loadAllTasks(ctx);
|
|
213
|
+
const items = await loadAllItems(ctx);
|
|
214
|
+
const index = new ReferenceIndex(tasks, items);
|
|
215
|
+
const ulids = [];
|
|
216
|
+
for (const ref of refs) {
|
|
217
|
+
const result = index.resolve(ref);
|
|
218
|
+
if (!result.ok) {
|
|
219
|
+
throw new Error(`Cannot resolve task reference: ${ref}`);
|
|
220
|
+
}
|
|
221
|
+
// Verify it's a task (not a spec item)
|
|
222
|
+
const task = tasks.find((t) => t._ulid === result.ulid);
|
|
223
|
+
if (!task) {
|
|
224
|
+
throw new Error(`Reference ${ref} is not a task`);
|
|
225
|
+
}
|
|
226
|
+
ulids.push(result.ulid);
|
|
227
|
+
}
|
|
228
|
+
return { refs, ulids };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Filter session context to only include tasks from explicit scope.
|
|
232
|
+
* AC: @cli-ralph ac-21
|
|
233
|
+
*/
|
|
234
|
+
function filterByExplicitTasks(ctx, scope) {
|
|
235
|
+
// Task refs in context are short ULIDs (variable length from shortUlid())
|
|
236
|
+
// Check if the context ref is a prefix of any explicit ULID
|
|
237
|
+
const matchesScope = (taskRef) => {
|
|
238
|
+
return scope.ulids.some((ulid) => ulid.startsWith(taskRef));
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
...ctx,
|
|
242
|
+
active_tasks: ctx.active_tasks.filter((t) => matchesScope(t.ref)),
|
|
243
|
+
pending_review_tasks: ctx.pending_review_tasks.filter((t) => matchesScope(t.ref)),
|
|
244
|
+
ready_tasks: ctx.ready_tasks.filter((t) => matchesScope(t.ref)),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if all explicit tasks are completed or blocked.
|
|
249
|
+
* AC: @cli-ralph ac-21
|
|
250
|
+
*/
|
|
251
|
+
async function allExplicitTasksDone(ctx, scope) {
|
|
252
|
+
const tasks = await loadAllTasks(ctx);
|
|
253
|
+
const statuses = new Map();
|
|
254
|
+
for (const ulid of scope.ulids) {
|
|
255
|
+
const task = tasks.find((t) => t._ulid === ulid);
|
|
256
|
+
if (task) {
|
|
257
|
+
statuses.set(ulid.slice(0, 8), task.status);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Check if all are completed or blocked
|
|
261
|
+
const done = scope.ulids.every((ulid) => {
|
|
262
|
+
const status = statuses.get(ulid.slice(0, 8));
|
|
263
|
+
return status === "completed" || status === "blocked";
|
|
264
|
+
});
|
|
265
|
+
return { done, statuses };
|
|
266
|
+
}
|
|
21
267
|
// ─── Prompt Template ─────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const focusSection = focus
|
|
268
|
+
// AC: @ralph-skill-delegation ac-1, ac-2, ac-3
|
|
269
|
+
function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, focus, explicitTaskScope) {
|
|
270
|
+
const focusSection = focus
|
|
271
|
+
? `
|
|
25
272
|
## Session Focus (applies to ALL iterations)
|
|
26
273
|
|
|
27
274
|
> **${focus}**
|
|
28
275
|
|
|
29
276
|
Keep this focus in mind throughout your work. It takes priority over default task selection.
|
|
30
|
-
`
|
|
31
|
-
|
|
277
|
+
`
|
|
278
|
+
: "";
|
|
279
|
+
// AC: @cli-ralph ac-21 - Explicit task scope indicator in prompt
|
|
280
|
+
const taskScopeSection = explicitTaskScope
|
|
281
|
+
? `
|
|
282
|
+
## Explicit Task Scope
|
|
283
|
+
|
|
284
|
+
This session is scoped to specific tasks: ${explicitTaskScope.refs.join(", ")}
|
|
285
|
+
|
|
286
|
+
**Only work on these tasks.** The loop will exit when all listed tasks are completed or blocked.
|
|
287
|
+
`
|
|
288
|
+
: "";
|
|
289
|
+
const modeDescription = explicitTaskScope
|
|
290
|
+
? "Loop mode means: no confirmations, auto-resolve decisions, explicit task scope (only the listed tasks)."
|
|
291
|
+
: "Loop mode means: no confirmations, auto-resolve decisions, automation-eligible tasks only.";
|
|
292
|
+
return `# Kspec Automation Session - Task Work
|
|
32
293
|
|
|
33
|
-
|
|
34
|
-
${
|
|
294
|
+
**Session ID:** \`${sessionId}\`
|
|
295
|
+
**Iteration:** ${iteration} of ${maxLoops}
|
|
296
|
+
**Mode:** Automated (no human in the loop)
|
|
297
|
+
${focusSection}${taskScopeSection}
|
|
35
298
|
|
|
36
299
|
## Current State
|
|
37
300
|
\`\`\`json
|
|
38
301
|
${JSON.stringify(sessionCtx, null, 2)}
|
|
39
302
|
\`\`\`
|
|
40
303
|
|
|
41
|
-
##
|
|
304
|
+
## Instructions
|
|
42
305
|
|
|
43
|
-
|
|
306
|
+
Run the task-work skill in loop mode:
|
|
44
307
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
\`\`\`
|
|
49
|
-
|
|
50
|
-
3. **Do the work**:
|
|
51
|
-
- Read relevant files to understand the task
|
|
52
|
-
- Make changes as needed
|
|
53
|
-
- Run tests if applicable
|
|
54
|
-
- Document as you go with task notes
|
|
308
|
+
\`\`\`
|
|
309
|
+
/task-work loop
|
|
310
|
+
\`\`\`
|
|
55
311
|
|
|
56
|
-
|
|
57
|
-
\`\`\`bash
|
|
58
|
-
kspec task note @task-ref "What you did, decisions made, etc."
|
|
59
|
-
\`\`\`
|
|
312
|
+
${modeDescription}
|
|
60
313
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
\`\`\`bash
|
|
64
|
-
kspec task submit @task-ref
|
|
65
|
-
\`\`\`
|
|
66
|
-
- If task is NOT done (WIP):
|
|
67
|
-
\`\`\`bash
|
|
68
|
-
kspec task note @task-ref "WIP: What's done, what remains..."
|
|
69
|
-
\`\`\`
|
|
314
|
+
**Normal flow:** Work on a task, create a PR, then stop responding. Ralph continues automatically —
|
|
315
|
+
it checks for remaining eligible tasks at the start of each iteration and exits the loop itself when none remain.
|
|
70
316
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
317
|
+
**Do NOT call \`end-loop\` after completing a task.** Simply stop responding.
|
|
318
|
+
\`end-loop\` is a rare escape hatch for when work is stalling across multiple iterations with no progress — not a normal exit path.
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Build the reflect prompt sent after task-work completes.
|
|
323
|
+
* Ralph sends this as a separate prompt to ensure reflection always happens.
|
|
324
|
+
*/
|
|
325
|
+
function buildReflectPrompt(iteration, maxLoops, sessionId) {
|
|
326
|
+
const isFinal = iteration === maxLoops;
|
|
327
|
+
return `# Kspec Automation Session - Reflection
|
|
74
328
|
|
|
75
|
-
|
|
76
|
-
|
|
329
|
+
**Session ID:** \`${sessionId}\`
|
|
330
|
+
**Iteration:** ${iteration} of ${maxLoops}
|
|
331
|
+
**Phase:** Post-task reflection
|
|
77
332
|
|
|
78
|
-
|
|
79
|
-
Think about what you learned, any friction points, or patterns worth remembering.
|
|
333
|
+
## Instructions
|
|
80
334
|
|
|
81
|
-
|
|
82
|
-
\`\`\`bash
|
|
83
|
-
kspec meta observe friction "Description of systemic issue..."
|
|
84
|
-
kspec meta observe success "Pattern worth replicating..."
|
|
85
|
-
\`\`\`
|
|
335
|
+
Run the reflect skill in loop mode:
|
|
86
336
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
\`\`\`
|
|
337
|
+
\`\`\`
|
|
338
|
+
/reflect loop
|
|
339
|
+
\`\`\`
|
|
91
340
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
This is the last iteration of the loop. After completing your work:
|
|
101
|
-
1. Commit any remaining changes
|
|
102
|
-
2. Reflect on the overall session
|
|
103
|
-
3. Capture any final insights as observations
|
|
104
|
-
` : ''}`;
|
|
341
|
+
Loop mode means: high-confidence captures only, must search existing before capturing, no user prompts.
|
|
342
|
+
${isFinal
|
|
343
|
+
? `
|
|
344
|
+
**FINAL ITERATION** - This is the last chance to capture insights from this session.
|
|
345
|
+
`
|
|
346
|
+
: ""}
|
|
347
|
+
Exit when reflection is complete.
|
|
348
|
+
`;
|
|
105
349
|
}
|
|
106
350
|
// ─── Streaming Output ────────────────────────────────────────────────────────
|
|
107
351
|
// Translator and renderer are created per-session in the action handler.
|
|
@@ -117,9 +361,9 @@ This is the last iteration of the loop. After completing your work:
|
|
|
117
361
|
function validateAdapter(adapterPackage) {
|
|
118
362
|
// Use npx --no-install with --version to check if package exists
|
|
119
363
|
// This checks both global and local node_modules, handles scoped packages
|
|
120
|
-
const result = spawnSync(
|
|
121
|
-
encoding:
|
|
122
|
-
stdio:
|
|
364
|
+
const result = spawnSync("npx", ["--no-install", adapterPackage, "--version"], {
|
|
365
|
+
encoding: "utf-8",
|
|
366
|
+
stdio: "pipe",
|
|
123
367
|
});
|
|
124
368
|
if (result.status !== 0) {
|
|
125
369
|
error(`Adapter package not found: ${adapterPackage}. Install with: npm install -g ${adapterPackage}`);
|
|
@@ -132,48 +376,50 @@ function validateAdapter(adapterPackage) {
|
|
|
132
376
|
* Implements file operations, terminal commands, and permission handling.
|
|
133
377
|
*/
|
|
134
378
|
async function handleRequest(client, id, method, params, yolo) {
|
|
135
|
-
const p = params;
|
|
136
379
|
try {
|
|
137
380
|
switch (method) {
|
|
138
|
-
case
|
|
381
|
+
case "session/request_permission": {
|
|
382
|
+
const p = params;
|
|
139
383
|
// In yolo mode, auto-approve all permissions
|
|
140
384
|
// In normal mode, would need to implement permission UI
|
|
141
385
|
const options = p.options || [];
|
|
142
386
|
if (yolo) {
|
|
143
387
|
// Find an "allow" option (prefer allow_always, then allow_once)
|
|
144
|
-
const allowOption = options.find(o => o.kind ===
|
|
145
|
-
|
|
388
|
+
const allowOption = options.find((o) => o.kind === "allow_always") ||
|
|
389
|
+
options.find((o) => o.kind === "allow_once");
|
|
146
390
|
if (allowOption) {
|
|
147
391
|
client.respondPermission(id, {
|
|
148
|
-
outcome: { outcome:
|
|
392
|
+
outcome: { outcome: "selected", optionId: allowOption.optionId },
|
|
149
393
|
});
|
|
150
394
|
}
|
|
151
395
|
else {
|
|
152
396
|
// No allow option available - cancel
|
|
153
|
-
client.respondPermission(id, { outcome: { outcome:
|
|
397
|
+
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
154
398
|
}
|
|
155
399
|
}
|
|
156
400
|
else {
|
|
157
401
|
// TODO: Implement permission prompting
|
|
158
|
-
client.respondPermission(id, { outcome: { outcome:
|
|
402
|
+
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
159
403
|
}
|
|
160
404
|
break;
|
|
161
405
|
}
|
|
162
|
-
case
|
|
163
|
-
const
|
|
164
|
-
const content = await fs.readFile(
|
|
165
|
-
client.
|
|
406
|
+
case "file/read": {
|
|
407
|
+
const p = params;
|
|
408
|
+
const content = await fs.readFile(p.path, "utf-8");
|
|
409
|
+
client.respondReadTextFile(id, { content });
|
|
166
410
|
break;
|
|
167
411
|
}
|
|
168
|
-
case
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
await fs.
|
|
172
|
-
|
|
173
|
-
client.respond(id, {});
|
|
412
|
+
case "file/write": {
|
|
413
|
+
const p = params;
|
|
414
|
+
await fs.mkdir(path.dirname(p.path), { recursive: true });
|
|
415
|
+
await fs.writeFile(p.path, p.content, "utf-8");
|
|
416
|
+
client.respondWriteTextFile(id, {});
|
|
174
417
|
break;
|
|
175
418
|
}
|
|
176
|
-
case
|
|
419
|
+
case "terminal/run": {
|
|
420
|
+
// Custom method (not part of ACP spec - ACP uses createTerminal instead)
|
|
421
|
+
// TODO: Consider migrating to standard ACP terminal methods
|
|
422
|
+
const p = params;
|
|
177
423
|
const command = p.command;
|
|
178
424
|
const cwd = p.cwd || process.cwd();
|
|
179
425
|
const timeout = p.timeout || 60000;
|
|
@@ -183,21 +429,22 @@ async function handleRequest(client, id, method, params, yolo) {
|
|
|
183
429
|
shell: true,
|
|
184
430
|
timeout,
|
|
185
431
|
});
|
|
186
|
-
let stdout =
|
|
187
|
-
let stderr =
|
|
188
|
-
child.stdout?.on(
|
|
432
|
+
let stdout = "";
|
|
433
|
+
let stderr = "";
|
|
434
|
+
child.stdout?.on("data", (data) => {
|
|
189
435
|
stdout += data.toString();
|
|
190
436
|
});
|
|
191
|
-
child.stderr?.on(
|
|
437
|
+
child.stderr?.on("data", (data) => {
|
|
192
438
|
stderr += data.toString();
|
|
193
439
|
});
|
|
194
|
-
child.on(
|
|
440
|
+
child.on("close", (code) => {
|
|
195
441
|
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
196
442
|
});
|
|
197
|
-
child.on(
|
|
443
|
+
child.on("error", (err) => {
|
|
198
444
|
resolve({ stdout, stderr: err.message, exitCode: 1 });
|
|
199
445
|
});
|
|
200
446
|
});
|
|
447
|
+
// Using generic respond() since this is a custom method
|
|
201
448
|
client.respond(id, result);
|
|
202
449
|
break;
|
|
203
450
|
}
|
|
@@ -211,65 +458,439 @@ async function handleRequest(client, id, method, params, yolo) {
|
|
|
211
458
|
client.respondError(id, -32000, message);
|
|
212
459
|
}
|
|
213
460
|
}
|
|
461
|
+
// ─── Subagent Support ─────────────────────────────────────────────────────────
|
|
462
|
+
/**
|
|
463
|
+
* Build context for a PR review subagent.
|
|
464
|
+
* AC: @ralph-subagent-spawning ac-10
|
|
465
|
+
*/
|
|
466
|
+
async function buildSubagentContext(ctx, taskRef) {
|
|
467
|
+
// Load all tasks and items
|
|
468
|
+
const tasks = await loadAllTasks(ctx);
|
|
469
|
+
const items = await loadAllItems(ctx);
|
|
470
|
+
const index = new ReferenceIndex(tasks, items);
|
|
471
|
+
// Resolve task reference
|
|
472
|
+
const taskResult = index.resolve(taskRef);
|
|
473
|
+
if (!taskResult.ok) {
|
|
474
|
+
throw new Error(`Task not found: ${taskRef}`);
|
|
475
|
+
}
|
|
476
|
+
const task = tasks.find((t) => t._ulid === taskResult.ulid);
|
|
477
|
+
if (!task) {
|
|
478
|
+
throw new Error(`Task not found by ULID: ${taskResult.ulid}`);
|
|
479
|
+
}
|
|
480
|
+
// Get linked spec with ACs if spec_ref exists
|
|
481
|
+
let specWithACs = null;
|
|
482
|
+
if (task.spec_ref) {
|
|
483
|
+
const specResult = index.resolve(task.spec_ref);
|
|
484
|
+
if (specResult.ok) {
|
|
485
|
+
const item = items.find((i) => i._ulid === specResult.ulid);
|
|
486
|
+
if (item) {
|
|
487
|
+
specWithACs = item;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Get git branch
|
|
492
|
+
const gitBranch = getCurrentBranch(ctx.rootDir) || "unknown";
|
|
493
|
+
return {
|
|
494
|
+
taskRef,
|
|
495
|
+
taskDetails: task,
|
|
496
|
+
specWithACs,
|
|
497
|
+
gitBranch,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get the current status of a task.
|
|
502
|
+
* AC: @ralph-subagent-spawning ac-12
|
|
503
|
+
*/
|
|
504
|
+
function getTaskStatus(taskRef) {
|
|
505
|
+
const result = spawnSync("kspec", ["task", "get", taskRef, "--json"], {
|
|
506
|
+
encoding: "utf-8",
|
|
507
|
+
stdio: "pipe",
|
|
508
|
+
});
|
|
509
|
+
if (result.status !== 0) {
|
|
510
|
+
warn(`Failed to check task status for ${taskRef}: ${result.stderr}`);
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
return JSON.parse(result.stdout).status;
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
warn(`Failed to parse task status for ${taskRef}`);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Mark a task as needing review due to subagent timeout.
|
|
523
|
+
* AC: @ralph-subagent-spawning ac-9
|
|
524
|
+
*/
|
|
525
|
+
async function markTaskNeedsReview(taskRef, reason) {
|
|
526
|
+
const { spawnSync } = await import("node:child_process");
|
|
527
|
+
// Use kspec CLI to set automation status
|
|
528
|
+
const result = spawnSync("kspec", ["task", "set-automation", taskRef, "needs_review"], {
|
|
529
|
+
encoding: "utf-8",
|
|
530
|
+
stdio: "pipe",
|
|
531
|
+
});
|
|
532
|
+
if (result.status !== 0) {
|
|
533
|
+
warn(`Failed to mark task ${taskRef} as needs_review: ${result.stderr}`);
|
|
534
|
+
}
|
|
535
|
+
// Add a note explaining the timeout
|
|
536
|
+
const noteResult = spawnSync("kspec", ["task", "note", taskRef, `[RALPH SUBAGENT] ${reason}`], {
|
|
537
|
+
encoding: "utf-8",
|
|
538
|
+
stdio: "pipe",
|
|
539
|
+
});
|
|
540
|
+
if (noteResult.status !== 0) {
|
|
541
|
+
warn(`Failed to add timeout note to task ${taskRef}: ${noteResult.stderr}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Post a comment on the open PR for a task's branch, noting incomplete review.
|
|
546
|
+
* Uses `gh pr list --head <branch>` to find the PR and add a warning.
|
|
547
|
+
*/
|
|
548
|
+
async function commentOnPRReviewIncomplete(branch, reason) {
|
|
549
|
+
if (!branch || branch === "unknown") {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const prListResult = spawnSync("gh", ["pr", "list", "--state", "open", "--head", branch, "--json", "number", "--jq", ".[0].number"], { encoding: "utf-8", stdio: "pipe" });
|
|
553
|
+
const prNumber = prListResult.stdout?.trim();
|
|
554
|
+
if (!prNumber || prListResult.status !== 0) {
|
|
555
|
+
// No open PR found — may already be merged or branch has no PR
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const body = `⚠️ **Review incomplete**: ${reason}\n\nThis PR was not fully reviewed by the ralph review subagent. Manual review recommended before merging.`;
|
|
559
|
+
const commentResult = spawnSync("gh", ["pr", "comment", prNumber, "--body", body], { encoding: "utf-8", stdio: "pipe" });
|
|
560
|
+
if (commentResult.status !== 0) {
|
|
561
|
+
warn(`Failed to comment on PR #${prNumber}: ${commentResult.stderr}`);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
info(`${DEFAULT_SUBAGENT_PREFIX} Posted review-incomplete comment on PR #${prNumber}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Handle failed iteration by tracking per-task failures and escalating at threshold.
|
|
569
|
+
* AC: @loop-mode-error-handling ac-1, ac-2, ac-3, ac-4, ac-5, ac-8
|
|
570
|
+
*/
|
|
571
|
+
async function handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDescription) {
|
|
572
|
+
if (tasksInProgressAtStart.length === 0) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Re-load current tasks to check progress
|
|
576
|
+
const currentTasks = await loadAllTasks(ctx);
|
|
577
|
+
const index = new ReferenceIndex(currentTasks, await loadAllItems(ctx));
|
|
578
|
+
// Convert ActiveTaskSummary to Task-like objects for processing
|
|
579
|
+
const tasksInProgressFull = tasksInProgressAtStart
|
|
580
|
+
.map((summary) => {
|
|
581
|
+
const resolved = index.resolve(summary.ref);
|
|
582
|
+
if (!resolved.ok)
|
|
583
|
+
return undefined;
|
|
584
|
+
// Check if the resolved item is a task (not a spec item or meta item)
|
|
585
|
+
const item = resolved.item;
|
|
586
|
+
if (!("status" in item))
|
|
587
|
+
return undefined; // Spec items don't have status
|
|
588
|
+
return currentTasks.find((t) => t._ulid === resolved.ulid);
|
|
589
|
+
})
|
|
590
|
+
.filter((t) => t !== undefined && t.status === "in_progress");
|
|
591
|
+
if (tasksInProgressFull.length === 0) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// Process failures
|
|
595
|
+
const { processFailedIteration, createFailureNote, getTaskFailureCount } = await import("../../ralph/index.js");
|
|
596
|
+
const results = processFailedIteration(tasksInProgressFull, currentTasks, iterationStartTime, errorDescription);
|
|
597
|
+
// Add notes and escalate tasks
|
|
598
|
+
for (const result of results) {
|
|
599
|
+
const taskRef = result.taskRef;
|
|
600
|
+
const task = currentTasks.find((t) => t._ulid === taskRef);
|
|
601
|
+
if (!task)
|
|
602
|
+
continue;
|
|
603
|
+
const priorCount = result.failureCount - 1;
|
|
604
|
+
const noteContent = createFailureNote(taskRef, errorDescription, priorCount);
|
|
605
|
+
// Add LOOP-FAIL note
|
|
606
|
+
const noteResult = spawnSync("kspec", ["task", "note", `@${taskRef}`, noteContent], {
|
|
607
|
+
encoding: "utf-8",
|
|
608
|
+
stdio: "pipe",
|
|
609
|
+
cwd: process.cwd(),
|
|
610
|
+
});
|
|
611
|
+
if (noteResult.status !== 0) {
|
|
612
|
+
warn(`Failed to add failure note to task ${taskRef}: ${noteResult.stderr}`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
// AC: @loop-mode-error-handling ac-5 - Escalate at threshold
|
|
616
|
+
if (result.escalated) {
|
|
617
|
+
const escalateResult = spawnSync("kspec", [
|
|
618
|
+
"task",
|
|
619
|
+
"set",
|
|
620
|
+
`@${taskRef}`,
|
|
621
|
+
"--automation",
|
|
622
|
+
"needs_review",
|
|
623
|
+
"--reason",
|
|
624
|
+
`Loop mode: 3 consecutive failures without progress`,
|
|
625
|
+
], {
|
|
626
|
+
encoding: "utf-8",
|
|
627
|
+
stdio: "pipe",
|
|
628
|
+
cwd: process.cwd(),
|
|
629
|
+
});
|
|
630
|
+
if (escalateResult.status !== 0) {
|
|
631
|
+
warn(`Failed to escalate task ${taskRef}: ${escalateResult.stderr}`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
info(`Escalated task ${taskRef} to automation:needs_review after 3 failures`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Process pending_review tasks by spawning subagents.
|
|
641
|
+
* AC: @ralph-subagent-spawning ac-6, ac-8
|
|
642
|
+
*/
|
|
643
|
+
async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, options, consecutiveFailures) {
|
|
644
|
+
if (pendingReviewTasks.length === 0) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
// Visual separator for subagent section
|
|
648
|
+
console.log("");
|
|
649
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
650
|
+
console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Processing Pending Review Tasks`));
|
|
651
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
652
|
+
console.log("");
|
|
653
|
+
info(`${DEFAULT_SUBAGENT_PREFIX} Found ${pendingReviewTasks.length} pending_review task(s)`);
|
|
654
|
+
// AC: @ralph-subagent-spawning ac-6 - Process one at a time
|
|
655
|
+
for (const task of pendingReviewTasks) {
|
|
656
|
+
info(`${DEFAULT_SUBAGENT_PREFIX} Processing: ${task.ref} - ${task.title}`);
|
|
657
|
+
try {
|
|
658
|
+
// Build context for this task
|
|
659
|
+
const subagentCtx = await buildSubagentContext(ctx, task.ref);
|
|
660
|
+
// AC: @ralph-subagent-spawning ac-1, ac-3 - Spawn and wait
|
|
661
|
+
const result = await runSubagent(adapter, subagentCtx, {
|
|
662
|
+
timeout: options.subagentTimeout,
|
|
663
|
+
outputPrefix: DEFAULT_SUBAGENT_PREFIX,
|
|
664
|
+
}, {
|
|
665
|
+
yolo: options.yolo,
|
|
666
|
+
cwd: options.cwd,
|
|
667
|
+
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, options.yolo),
|
|
668
|
+
});
|
|
669
|
+
if (result.timedOut) {
|
|
670
|
+
// AC: @ralph-subagent-spawning ac-9
|
|
671
|
+
warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent timed out for ${task.ref}`);
|
|
672
|
+
const timeoutMinutes = Math.round(options.subagentTimeout / 60000);
|
|
673
|
+
await markTaskNeedsReview(task.ref, `Subagent timed out after ${timeoutMinutes} minutes`);
|
|
674
|
+
await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent timed out after ${timeoutMinutes} minutes for task ${task.ref}.`);
|
|
675
|
+
consecutiveFailures.count++;
|
|
676
|
+
}
|
|
677
|
+
else if (!result.success) {
|
|
678
|
+
// AC: @ralph-subagent-spawning ac-7
|
|
679
|
+
error(`${DEFAULT_SUBAGENT_PREFIX} Subagent failed for ${task.ref}: ${result.error}`);
|
|
680
|
+
await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent failed for task ${task.ref}: ${result.error}`);
|
|
681
|
+
consecutiveFailures.count++;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
// AC: @ralph-subagent-spawning ac-12 - Verify task outcome
|
|
685
|
+
const currentStatus = getTaskStatus(task.ref);
|
|
686
|
+
if (currentStatus === "completed") {
|
|
687
|
+
success(`${DEFAULT_SUBAGENT_PREFIX} Completed: ${task.ref}`);
|
|
688
|
+
consecutiveFailures.count = 0;
|
|
689
|
+
}
|
|
690
|
+
else if (currentStatus === "needs_work") {
|
|
691
|
+
// Expected: reviewer found issues, kicked back to worker
|
|
692
|
+
info(`${DEFAULT_SUBAGENT_PREFIX} Review completed for ${task.ref} — issues found, kicked back to worker`);
|
|
693
|
+
// NOT a failure — the review worked correctly
|
|
694
|
+
consecutiveFailures.count = 0;
|
|
695
|
+
}
|
|
696
|
+
else if (currentStatus === "pending_review") {
|
|
697
|
+
// Subagent didn't transition or merge — count as soft failure
|
|
698
|
+
warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent completed but task ${task.ref} unchanged`);
|
|
699
|
+
await markTaskNeedsReview(task.ref, "Subagent completed but did not merge or kick back. Review required.");
|
|
700
|
+
consecutiveFailures.count++;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
warn(`${DEFAULT_SUBAGENT_PREFIX} Task ${task.ref} in unexpected state: ${currentStatus}`);
|
|
704
|
+
consecutiveFailures.count++;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Check if we've hit max failures
|
|
708
|
+
if (consecutiveFailures.count >= options.maxFailures) {
|
|
709
|
+
error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
715
|
+
error(`${DEFAULT_SUBAGENT_PREFIX} Error processing ${task.ref}: ${message}`);
|
|
716
|
+
consecutiveFailures.count++;
|
|
717
|
+
if (consecutiveFailures.count >= options.maxFailures) {
|
|
718
|
+
error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Visual separator at end of subagent section
|
|
724
|
+
console.log("");
|
|
725
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
726
|
+
console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Completed Review Processing`));
|
|
727
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
728
|
+
console.log("");
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
214
731
|
// ─── Command Registration ────────────────────────────────────────────────────
|
|
215
732
|
export function registerRalphCommand(program) {
|
|
216
|
-
program
|
|
217
|
-
.command(
|
|
218
|
-
.description(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
.
|
|
223
|
-
.
|
|
224
|
-
.option(
|
|
225
|
-
.option('--adapter <id>', 'Agent adapter to use', 'claude-code-acp')
|
|
226
|
-
.option('--adapter-cmd <cmd>', 'Custom adapter command (for testing)')
|
|
227
|
-
.option('--focus <instructions>', 'Focus instructions included in every iteration prompt')
|
|
733
|
+
const ralph = program
|
|
734
|
+
.command("ralph")
|
|
735
|
+
.description("Ralph automated task loop and agent control");
|
|
736
|
+
// end-loop subcommand - allows agent to signal loop termination
|
|
737
|
+
// AC: @ralph-end-loop ac-cmd, ac-reason, ac-noop-outside
|
|
738
|
+
ralph
|
|
739
|
+
.command("end-loop")
|
|
740
|
+
.description("End the ralph loop gracefully (stops all remaining iterations)")
|
|
741
|
+
.option("--reason <reason>", "Reason for ending the loop")
|
|
228
742
|
.action(async (options) => {
|
|
743
|
+
try {
|
|
744
|
+
const ctx = await initContext();
|
|
745
|
+
// Check if we're in a ralph session by looking for any ralph marker
|
|
746
|
+
const taskLimitMarker = await readTaskLimitMarker(ctx.rootDir);
|
|
747
|
+
const endLoopMarker = await readEndLoopMarker(ctx.rootDir);
|
|
748
|
+
// Write the marker with reason if provided
|
|
749
|
+
await writeEndLoopMarker(ctx.rootDir, options.reason);
|
|
750
|
+
// Determine if we're likely in a ralph session
|
|
751
|
+
const inRalphSession = taskLimitMarker !== null || endLoopMarker !== null;
|
|
752
|
+
if (!inRalphSession) {
|
|
753
|
+
// AC: @ralph-end-loop ac-noop-outside
|
|
754
|
+
warn("No active ralph session detected. Marker written but may have no effect.");
|
|
755
|
+
info("This command is designed to be called by agents during a ralph loop.");
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
success("Loop end signal sent");
|
|
759
|
+
}
|
|
760
|
+
if (options.reason) {
|
|
761
|
+
info(`Reason: ${options.reason}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
error("Failed to signal end-loop", err);
|
|
766
|
+
process.exit(EXIT_CODES.ERROR);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
// Main ralph run command (default behavior when ralph is called directly)
|
|
770
|
+
ralph
|
|
771
|
+
.command("run", { isDefault: true })
|
|
772
|
+
.description("Run ACP agent in a loop to process ready tasks")
|
|
773
|
+
.argument("[args...]", "")
|
|
774
|
+
.option("--max-loops <n>", "Maximum iterations", "5")
|
|
775
|
+
.option("--max-retries <n>", "Max retries per iteration on error", "3")
|
|
776
|
+
.option("--max-failures <n>", "Max consecutive failed iterations before exit", "3")
|
|
777
|
+
.option("--dry-run", "Show prompt without executing")
|
|
778
|
+
.option("--yolo", "Use dangerously-skip-permissions (default)", true)
|
|
779
|
+
.option("--no-yolo", "Require normal permission prompts")
|
|
780
|
+
.option("--subagent-timeout <minutes>", "Review subagent timeout in minutes", "20")
|
|
781
|
+
.option("--adapter <id>", "Agent adapter to use", "claude-agent-acp")
|
|
782
|
+
.option("--adapter-cmd <cmd>", "Custom adapter command (for testing)")
|
|
783
|
+
.option("--restart-every <n>", "Restart agent every N iterations to prevent OOM (0 = never)", "10")
|
|
784
|
+
.option("--focus <instructions>", "Focus instructions included in every iteration prompt")
|
|
785
|
+
.option("--max-tasks <n>", "Max tasks per iteration (0 = unlimited)", "1")
|
|
786
|
+
.option("--tasks <refs>", "Explicit task scope: only work on these tasks (comma-separated refs, e.g., @task1,@task2)")
|
|
787
|
+
.action(async (args, options) => {
|
|
788
|
+
// Check for unknown subcommands that fell through to default
|
|
789
|
+
// Only check args that look like subcommand names (alphanumeric with hyphens, no quotes)
|
|
790
|
+
if (args.length > 0) {
|
|
791
|
+
const unknownCmd = args[0];
|
|
792
|
+
// Skip if it looks like a malformed option or quoted argument
|
|
793
|
+
const looksLikeSubcommand = /^[a-z][a-z0-9-]*$/i.test(unknownCmd);
|
|
794
|
+
if (looksLikeSubcommand) {
|
|
795
|
+
if (unknownCmd === "end-iteration") {
|
|
796
|
+
error(`Unknown command: ${unknownCmd}. Did you mean 'end-loop'?`);
|
|
797
|
+
info("The command was renamed from 'end-iteration' to 'end-loop' to clarify it ends the entire loop.");
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
error(`Unknown command: ${unknownCmd}`);
|
|
801
|
+
}
|
|
802
|
+
info("Run 'kspec ralph --help' to see available commands.");
|
|
803
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
229
806
|
try {
|
|
230
807
|
const maxLoops = parseInt(options.maxLoops, 10);
|
|
231
808
|
const maxRetries = parseInt(options.maxRetries, 10);
|
|
232
809
|
const maxFailures = parseInt(options.maxFailures, 10);
|
|
233
|
-
if (isNaN(maxLoops) || maxLoops < 1) {
|
|
810
|
+
if (Number.isNaN(maxLoops) || maxLoops < 1) {
|
|
234
811
|
error(errors.usage.maxLoopsPositive);
|
|
235
812
|
process.exit(EXIT_CODES.ERROR);
|
|
236
813
|
}
|
|
237
|
-
if (isNaN(maxRetries) || maxRetries < 0) {
|
|
814
|
+
if (Number.isNaN(maxRetries) || maxRetries < 0) {
|
|
238
815
|
error(errors.usage.maxRetriesNonNegative);
|
|
239
816
|
process.exit(EXIT_CODES.ERROR);
|
|
240
817
|
}
|
|
241
|
-
if (isNaN(maxFailures) || maxFailures < 1) {
|
|
818
|
+
if (Number.isNaN(maxFailures) || maxFailures < 1) {
|
|
242
819
|
error(errors.usage.maxFailuresPositive);
|
|
243
820
|
process.exit(EXIT_CODES.ERROR);
|
|
244
821
|
}
|
|
822
|
+
const subagentTimeout = parseInt(options.subagentTimeout, 10);
|
|
823
|
+
if (Number.isNaN(subagentTimeout) || subagentTimeout < 1) {
|
|
824
|
+
error("--subagent-timeout must be a positive integer (minutes)");
|
|
825
|
+
process.exit(EXIT_CODES.ERROR);
|
|
826
|
+
}
|
|
827
|
+
const restartEvery = parseInt(options.restartEvery, 10);
|
|
828
|
+
if (Number.isNaN(restartEvery) || restartEvery < 0) {
|
|
829
|
+
error("--restart-every must be a non-negative integer");
|
|
830
|
+
process.exit(EXIT_CODES.ERROR);
|
|
831
|
+
}
|
|
832
|
+
// AC: @ralph-task-limit ac-flag
|
|
833
|
+
const maxTasks = parseInt(options.maxTasks, 10);
|
|
834
|
+
if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
|
|
835
|
+
error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
|
|
836
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
837
|
+
}
|
|
245
838
|
// Handle custom adapter command for testing
|
|
246
839
|
if (options.adapterCmd) {
|
|
247
840
|
const parts = options.adapterCmd.split(/\s+/);
|
|
248
841
|
const customAdapter = {
|
|
249
842
|
command: parts[0],
|
|
250
843
|
args: parts.slice(1),
|
|
251
|
-
description:
|
|
844
|
+
description: "Custom adapter via --adapter-cmd",
|
|
252
845
|
};
|
|
253
|
-
registerAdapter(
|
|
254
|
-
options.adapter =
|
|
846
|
+
registerAdapter("custom", customAdapter);
|
|
847
|
+
options.adapter = "custom";
|
|
255
848
|
}
|
|
256
849
|
// Resolve adapter
|
|
257
850
|
const adapter = resolveAdapter(options.adapter);
|
|
258
851
|
// Validate adapter package exists before proceeding
|
|
259
|
-
// Skip validation for
|
|
260
|
-
|
|
852
|
+
// Skip validation for:
|
|
853
|
+
// - Custom adapters (--adapter-cmd)
|
|
854
|
+
// - Non-npx adapters
|
|
855
|
+
// - Dry-run mode with default adapter (doesn't spawn agent, default may not be installed in CI)
|
|
856
|
+
// Note: If user explicitly specifies --adapter, validate even in dry-run to catch typos
|
|
857
|
+
// Accept both new and deprecated adapter names
|
|
858
|
+
const isDefaultAdapter = options.adapter === "claude-agent-acp" ||
|
|
859
|
+
options.adapter === "claude-code-acp";
|
|
860
|
+
const skipValidation = options.adapterCmd ||
|
|
861
|
+
adapter.command !== "npx" ||
|
|
862
|
+
!adapter.args[0] ||
|
|
863
|
+
(options.dryRun && isDefaultAdapter);
|
|
864
|
+
if (!skipValidation) {
|
|
261
865
|
validateAdapter(adapter.args[0]);
|
|
262
866
|
}
|
|
263
|
-
// Add yolo flag to adapter args if needed
|
|
264
|
-
if (options.yolo &&
|
|
265
|
-
adapter.args = [...adapter.args,
|
|
867
|
+
// Add yolo flag to adapter args if needed (accept both new and deprecated names)
|
|
868
|
+
if (options.yolo && isDefaultAdapter) {
|
|
869
|
+
adapter.args = [...adapter.args, "--dangerously-skip-permissions"];
|
|
870
|
+
}
|
|
871
|
+
const restartInfo = restartEvery > 0 ? `, restart every ${restartEvery}` : "";
|
|
872
|
+
const maxTasksInfo = maxTasks === 0 ? "unlimited" : `${maxTasks}`;
|
|
873
|
+
// Initialize kspec context early to validate --tasks
|
|
874
|
+
const ctx = await initContext();
|
|
875
|
+
// AC: @cli-ralph ac-21 - Parse explicit task scope
|
|
876
|
+
let explicitTaskScope;
|
|
877
|
+
if (options.tasks) {
|
|
878
|
+
try {
|
|
879
|
+
explicitTaskScope = await parseExplicitTasks(ctx, options.tasks);
|
|
880
|
+
info(`Explicit task scope: ${explicitTaskScope.refs.join(", ")}`);
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
error(`Invalid --tasks argument: ${err.message}`);
|
|
884
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
885
|
+
}
|
|
266
886
|
}
|
|
267
|
-
|
|
887
|
+
const taskScopeInfo = explicitTaskScope
|
|
888
|
+
? `, tasks=${explicitTaskScope.refs.join(",")}`
|
|
889
|
+
: "";
|
|
890
|
+
info(`Starting ralph loop (adapter=${options.adapter}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures${restartInfo}, max-tasks=${maxTasksInfo}${taskScopeInfo})`);
|
|
268
891
|
if (options.focus) {
|
|
269
892
|
info(`Focus: ${options.focus}`);
|
|
270
893
|
}
|
|
271
|
-
// Initialize kspec context
|
|
272
|
-
const ctx = await initContext();
|
|
273
894
|
const specDir = ctx.specDir;
|
|
274
895
|
// Create session for event tracking
|
|
275
896
|
const sessionId = ulid();
|
|
@@ -281,53 +902,181 @@ export function registerRalphCommand(program) {
|
|
|
281
902
|
// Log session start
|
|
282
903
|
await appendEvent(specDir, {
|
|
283
904
|
session_id: sessionId,
|
|
284
|
-
type:
|
|
905
|
+
type: "session.start",
|
|
285
906
|
data: {
|
|
286
907
|
adapter: options.adapter,
|
|
287
908
|
maxLoops,
|
|
288
909
|
maxRetries,
|
|
289
910
|
maxFailures,
|
|
911
|
+
maxTasks,
|
|
290
912
|
yolo: options.yolo,
|
|
291
913
|
focus: options.focus,
|
|
914
|
+
explicitTasks: explicitTaskScope?.refs,
|
|
292
915
|
},
|
|
293
916
|
});
|
|
294
917
|
let consecutiveFailures = 0;
|
|
295
918
|
let agent = null;
|
|
296
919
|
let acpSessionId = null;
|
|
920
|
+
// AC: @ralph-end-loop ac-signal-cleanup, @ralph-task-limit ac-signal-cleanup
|
|
921
|
+
// Signal handlers for cleanup on Ctrl+C or kill
|
|
922
|
+
// Note: Signal handlers must be synchronous, so we use Promise.finally()
|
|
923
|
+
// to ensure cleanup completes before exit
|
|
924
|
+
const signalCleanup = (signal) => {
|
|
925
|
+
info(`Received ${signal}, cleaning up...`);
|
|
926
|
+
// Kill agent if running
|
|
927
|
+
if (agent) {
|
|
928
|
+
agent.kill();
|
|
929
|
+
}
|
|
930
|
+
// Clean up marker files, then exit after cleanup completes
|
|
931
|
+
Promise.all([
|
|
932
|
+
clearTaskLimitMarker(ctx.rootDir),
|
|
933
|
+
clearEndLoopMarker(ctx.rootDir),
|
|
934
|
+
]).finally(() => {
|
|
935
|
+
process.exit(0);
|
|
936
|
+
});
|
|
937
|
+
};
|
|
938
|
+
const sigintHandler = () => { signalCleanup("SIGINT"); };
|
|
939
|
+
const sigtermHandler = () => { signalCleanup("SIGTERM"); };
|
|
940
|
+
process.on("SIGINT", sigintHandler);
|
|
941
|
+
process.on("SIGTERM", sigtermHandler);
|
|
297
942
|
// Create translator and renderer for this session
|
|
298
943
|
const translator = createTranslator();
|
|
299
944
|
const renderer = createCliRenderer();
|
|
945
|
+
// Task limit state - tracks completions per iteration
|
|
946
|
+
// AC: @ralph-task-limit ac-reset, ac-wrapup
|
|
947
|
+
let taskLimitReached = false;
|
|
948
|
+
let tasksCompletedThisIteration = 0;
|
|
949
|
+
// End-loop signal state
|
|
950
|
+
// AC: @ralph-end-loop ac-detect, ac-graceful
|
|
951
|
+
let endLoopRequested = false;
|
|
952
|
+
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1 - Track exit reason for wrap-up
|
|
953
|
+
let exitReason = null;
|
|
954
|
+
let lastIterationCtx = null;
|
|
955
|
+
let lastErrorMessage;
|
|
956
|
+
const recentTaskRefs = [];
|
|
300
957
|
try {
|
|
301
958
|
for (let iteration = 1; iteration <= maxLoops; iteration++) {
|
|
302
959
|
renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
960
|
+
// AC: @ralph-task-limit ac-reset - Reset counter and clear stale markers at iteration start
|
|
961
|
+
taskLimitReached = false;
|
|
962
|
+
tasksCompletedThisIteration = 0;
|
|
963
|
+
const wasStale = await clearStaleMarker(ctx.rootDir);
|
|
964
|
+
if (wasStale) {
|
|
965
|
+
info("Cleared stale task limit marker from previous session");
|
|
966
|
+
}
|
|
967
|
+
// Also clear any marker from previous iteration of this session
|
|
968
|
+
await clearTaskLimitMarker(ctx.rootDir);
|
|
969
|
+
// AC: @ralph-end-loop ac-cleanup - Reset end-loop state
|
|
970
|
+
endLoopRequested = false;
|
|
971
|
+
const wasStaleEndLoop = await clearStaleEndLoopMarker(ctx.rootDir);
|
|
972
|
+
if (wasStaleEndLoop) {
|
|
973
|
+
info("Cleared stale end-loop marker from previous session");
|
|
974
|
+
}
|
|
975
|
+
await clearEndLoopMarker(ctx.rootDir);
|
|
976
|
+
// Gather fresh context each iteration
|
|
977
|
+
// AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
|
|
978
|
+
// AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
|
|
979
|
+
let sessionCtx = await gatherSessionContext(ctx, {
|
|
980
|
+
limit: "10",
|
|
981
|
+
eligible: !explicitTaskScope, // Skip eligibility filter if explicit scope
|
|
982
|
+
});
|
|
983
|
+
// AC: @cli-ralph ac-21 - Filter to explicit tasks if scope is set
|
|
984
|
+
if (explicitTaskScope) {
|
|
985
|
+
sessionCtx = filterByExplicitTasks(sessionCtx, explicitTaskScope);
|
|
986
|
+
}
|
|
987
|
+
// AC: @ralph-subagent-spawning ac-8 - Process pending_review tasks BEFORE main iteration
|
|
988
|
+
// This wraps consecutiveFailures in an object so it can be mutated by the helper
|
|
989
|
+
const failureTracker = { count: consecutiveFailures };
|
|
990
|
+
const continueLoop = await processPendingReviewTasks(ctx, adapter, sessionCtx.pending_review_tasks, {
|
|
991
|
+
yolo: options.yolo,
|
|
992
|
+
maxRetries,
|
|
993
|
+
maxFailures,
|
|
994
|
+
cwd: process.cwd(),
|
|
995
|
+
subagentTimeout: subagentTimeout * 60 * 1000,
|
|
996
|
+
}, failureTracker);
|
|
997
|
+
consecutiveFailures = failureTracker.count;
|
|
998
|
+
if (!continueLoop) {
|
|
999
|
+
exitReason = "max_failures";
|
|
1000
|
+
lastIterationCtx = sessionCtx;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
// AC: @cli-ralph ac-20 - Refresh context after pending_review processing
|
|
1004
|
+
// If pending_review tasks were processed, they may have completed and unblocked
|
|
1005
|
+
// dependent tasks. Re-gather context to detect newly available tasks.
|
|
1006
|
+
let currentCtx = sessionCtx;
|
|
1007
|
+
if (sessionCtx.pending_review_tasks.length > 0) {
|
|
1008
|
+
currentCtx = await gatherSessionContext(ctx, {
|
|
1009
|
+
limit: "10",
|
|
1010
|
+
eligible: !explicitTaskScope,
|
|
1011
|
+
});
|
|
1012
|
+
if (explicitTaskScope) {
|
|
1013
|
+
currentCtx = filterByExplicitTasks(currentCtx, explicitTaskScope);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// AC: @cli-ralph ac-21 - Check explicit task completion
|
|
1017
|
+
if (explicitTaskScope) {
|
|
1018
|
+
const { done, statuses } = await allExplicitTasksDone(ctx, explicitTaskScope);
|
|
1019
|
+
if (done) {
|
|
1020
|
+
const statusList = Array.from(statuses.entries())
|
|
1021
|
+
.map(([ref, status]) => `${ref}: ${status}`)
|
|
1022
|
+
.join(", ");
|
|
1023
|
+
info(`All explicit tasks completed or blocked (${statusList}). Exiting loop.`);
|
|
1024
|
+
exitReason = "explicit_tasks_done";
|
|
1025
|
+
lastIterationCtx = currentCtx;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Check for automation-eligible tasks (ready or in_progress)
|
|
1030
|
+
// AC: @cli-ralph ac-19
|
|
1031
|
+
const hasActiveTasks = currentCtx.active_tasks.length > 0;
|
|
1032
|
+
const hasReadyTasks = currentCtx.ready_tasks.length > 0;
|
|
309
1033
|
if (!hasActiveTasks && !hasReadyTasks) {
|
|
310
|
-
|
|
1034
|
+
if (explicitTaskScope) {
|
|
1035
|
+
info("No explicit tasks available (ready or in_progress). Exiting loop.");
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
info("No automation-eligible tasks (ready or in_progress). Exiting loop.");
|
|
1039
|
+
}
|
|
1040
|
+
exitReason = "no_tasks";
|
|
1041
|
+
lastIterationCtx = currentCtx;
|
|
311
1042
|
break;
|
|
312
1043
|
}
|
|
313
|
-
//
|
|
314
|
-
const
|
|
1044
|
+
// AC: @loop-mode-error-handling - Track tasks in progress for failure handling
|
|
1045
|
+
const tasksInProgressAtStart = sessionCtx.active_tasks;
|
|
1046
|
+
const iterationStartTime = new Date();
|
|
1047
|
+
// Build prompts - task-work first, then reflect
|
|
1048
|
+
// AC: @cli-ralph ac-21 - Include explicit task scope in prompt
|
|
1049
|
+
const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, options.focus, explicitTaskScope);
|
|
1050
|
+
const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId);
|
|
1051
|
+
// AC: @ralph-task-limit ac-dryrun, @cli-ralph ac-21
|
|
315
1052
|
if (options.dryRun) {
|
|
316
|
-
console.log(chalk.yellow(
|
|
317
|
-
console.log(
|
|
318
|
-
console.log(
|
|
1053
|
+
console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
|
|
1054
|
+
console.log(` max-loops: ${maxLoops}`);
|
|
1055
|
+
console.log(` max-tasks: ${maxTasks === 0 ? "unlimited" : maxTasks}`);
|
|
1056
|
+
console.log(` max-retries: ${maxRetries}`);
|
|
1057
|
+
console.log(` max-failures: ${maxFailures}`);
|
|
1058
|
+
console.log(` restart-every: ${restartEvery === 0 ? "never" : restartEvery}`);
|
|
1059
|
+
if (explicitTaskScope) {
|
|
1060
|
+
console.log(` explicit-tasks: ${explicitTaskScope.refs.join(", ")}`);
|
|
1061
|
+
}
|
|
1062
|
+
console.log(chalk.yellow("\n=== Task Work Prompt ===\n"));
|
|
1063
|
+
console.log(taskWorkPrompt);
|
|
1064
|
+
console.log(chalk.yellow("\n=== Reflect Prompt ===\n"));
|
|
1065
|
+
console.log(reflectPrompt);
|
|
1066
|
+
console.log(chalk.yellow("\n=== END DRY RUN ==="));
|
|
319
1067
|
break;
|
|
320
1068
|
}
|
|
321
|
-
// Log prompt
|
|
1069
|
+
// Log task-work prompt
|
|
322
1070
|
await appendEvent(specDir, {
|
|
323
1071
|
session_id: sessionId,
|
|
324
|
-
type:
|
|
1072
|
+
type: "prompt.sent",
|
|
325
1073
|
data: {
|
|
326
1074
|
iteration,
|
|
327
|
-
|
|
1075
|
+
phase: "task-work",
|
|
1076
|
+
prompt: taskWorkPrompt,
|
|
328
1077
|
tasks: {
|
|
329
|
-
active:
|
|
330
|
-
ready:
|
|
1078
|
+
active: currentCtx.active_tasks.map((t) => t.ref),
|
|
1079
|
+
ready: currentCtx.ready_tasks.map((t) => t.ref),
|
|
331
1080
|
},
|
|
332
1081
|
},
|
|
333
1082
|
});
|
|
@@ -341,63 +1090,156 @@ export function registerRalphCommand(program) {
|
|
|
341
1090
|
try {
|
|
342
1091
|
// Spawn agent if not already running
|
|
343
1092
|
if (!agent) {
|
|
344
|
-
info(
|
|
1093
|
+
info("Spawning ACP agent...");
|
|
345
1094
|
agent = await spawnAndInitialize(adapter, {
|
|
346
1095
|
cwd: process.cwd(),
|
|
347
1096
|
clientOptions: {
|
|
348
1097
|
clientInfo: {
|
|
349
|
-
name:
|
|
350
|
-
version:
|
|
1098
|
+
name: "kspec-ralph",
|
|
1099
|
+
version: packageVersion,
|
|
1100
|
+
},
|
|
1101
|
+
methodTimeouts: {
|
|
1102
|
+
"session/prompt": RALPH_PROMPT_TIMEOUT,
|
|
1103
|
+
"session/resume": RALPH_PROMPT_TIMEOUT,
|
|
351
1104
|
},
|
|
352
1105
|
},
|
|
353
1106
|
});
|
|
354
1107
|
// Set up streaming update handler with translator + renderer
|
|
355
|
-
agent.client.on(
|
|
1108
|
+
agent.client.on("update", (_sid, update) => {
|
|
356
1109
|
// Translate ACP event to RalphEvent and render
|
|
357
1110
|
const event = translator.translate(update);
|
|
358
1111
|
if (event) {
|
|
359
1112
|
renderer.render(event);
|
|
360
1113
|
}
|
|
1114
|
+
// AC: @ralph-task-limit ac-detection, ac-wrapup
|
|
1115
|
+
// Detect task completions for limit enforcement
|
|
1116
|
+
if (maxTasks > 0 && !taskLimitReached) {
|
|
1117
|
+
const bashCmd = extractBashCommand(update);
|
|
1118
|
+
if (bashCmd && detectTaskCompleteCommand(bashCmd)) {
|
|
1119
|
+
// Pattern matched - verify via kspec query
|
|
1120
|
+
getIterationStats(ctx, iterationStartTime)
|
|
1121
|
+
.then(async (stats) => {
|
|
1122
|
+
if (stats.tasks_completed >= maxTasks && !taskLimitReached) {
|
|
1123
|
+
taskLimitReached = true;
|
|
1124
|
+
tasksCompletedThisIteration = stats.tasks_completed;
|
|
1125
|
+
info(`Task limit reached (${stats.tasks_completed}/${maxTasks})`);
|
|
1126
|
+
// AC: @ralph-task-limit ac-marker-format, ac-wrapup
|
|
1127
|
+
// Write marker file for hook enforcement
|
|
1128
|
+
const marker = {
|
|
1129
|
+
active: true,
|
|
1130
|
+
since: iterationStartTime.toISOString(),
|
|
1131
|
+
max: maxTasks,
|
|
1132
|
+
completed: stats.tasks_completed,
|
|
1133
|
+
sessionId,
|
|
1134
|
+
};
|
|
1135
|
+
await writeTaskLimitMarker(ctx.rootDir, marker);
|
|
1136
|
+
// Inject wrap-up message to agent
|
|
1137
|
+
if (agent && acpSessionId) {
|
|
1138
|
+
const wrapUpMsg = `\n\n**TASK LIMIT REACHED** - ${stats.tasks_completed} task(s) completed this iteration (limit: ${maxTasks}).\n\nPlease wrap up your current work and exit cleanly. Do not start new tasks.\n\nCompleted tasks this iteration: ${stats.completed_refs.join(", ")}`;
|
|
1139
|
+
agent.client.prompt({
|
|
1140
|
+
sessionId: acpSessionId,
|
|
1141
|
+
prompt: [{ type: "text", text: wrapUpMsg }],
|
|
1142
|
+
}).catch(() => {
|
|
1143
|
+
// Ignore if message injection fails
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
})
|
|
1148
|
+
.catch(() => {
|
|
1149
|
+
// Ignore query failures - detection is best-effort
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// AC: @ralph-end-loop ac-detect
|
|
1154
|
+
// Detect explicit end-loop command
|
|
1155
|
+
if (!endLoopRequested) {
|
|
1156
|
+
const bashCmd = extractBashCommand(update);
|
|
1157
|
+
if (bashCmd && detectEndLoopCommand(bashCmd)) {
|
|
1158
|
+
endLoopRequested = true;
|
|
1159
|
+
// Read marker to get reason if present
|
|
1160
|
+
readEndLoopMarker(ctx.rootDir)
|
|
1161
|
+
.then((marker) => {
|
|
1162
|
+
const reason = marker?.reason
|
|
1163
|
+
? ` (${marker.reason})`
|
|
1164
|
+
: "";
|
|
1165
|
+
info(`End-loop signal received${reason}`);
|
|
1166
|
+
})
|
|
1167
|
+
.catch(() => {
|
|
1168
|
+
info("End-loop signal received");
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
361
1172
|
// Log raw update event (async, non-blocking)
|
|
362
1173
|
appendEvent(specDir, {
|
|
363
1174
|
session_id: sessionId,
|
|
364
|
-
type:
|
|
1175
|
+
type: "session.update",
|
|
365
1176
|
data: { iteration, update },
|
|
366
1177
|
}).catch(() => {
|
|
367
1178
|
// Ignore logging errors during streaming
|
|
368
1179
|
});
|
|
369
1180
|
});
|
|
370
1181
|
// Set up tool request handler
|
|
371
|
-
agent.client.on(
|
|
1182
|
+
agent.client.on("request", (reqId, method, params) => {
|
|
1183
|
+
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
372
1184
|
handleRequest(agent.client, reqId, method, params, options.yolo).catch((err) => {
|
|
1185
|
+
// biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
|
|
373
1186
|
agent.client.respondError(reqId, -32000, err.message);
|
|
374
1187
|
});
|
|
375
1188
|
});
|
|
376
1189
|
}
|
|
377
1190
|
// Create fresh ACP session per iteration to keep context clean
|
|
378
|
-
info(
|
|
1191
|
+
info("Creating ACP session...");
|
|
379
1192
|
acpSessionId = await agent.client.newSession({
|
|
380
1193
|
cwd: process.cwd(),
|
|
381
1194
|
mcpServers: [], // No MCP servers for now
|
|
382
1195
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
1196
|
+
// Phase 1: Task Work
|
|
1197
|
+
info("Sending task-work prompt to agent...");
|
|
1198
|
+
const taskWorkResponse = await agent.client.prompt({
|
|
386
1199
|
sessionId: acpSessionId,
|
|
387
|
-
prompt: [{ type:
|
|
1200
|
+
prompt: [{ type: "text", text: taskWorkPrompt }],
|
|
388
1201
|
});
|
|
389
|
-
// Log completion
|
|
1202
|
+
// Log task-work completion
|
|
390
1203
|
await appendEvent(specDir, {
|
|
391
1204
|
session_id: sessionId,
|
|
392
|
-
type:
|
|
1205
|
+
type: "session.update",
|
|
393
1206
|
data: {
|
|
394
1207
|
iteration,
|
|
395
|
-
|
|
1208
|
+
phase: "task-work",
|
|
1209
|
+
stopReason: taskWorkResponse.stopReason,
|
|
396
1210
|
completed: true,
|
|
397
1211
|
},
|
|
398
1212
|
});
|
|
399
|
-
|
|
400
|
-
|
|
1213
|
+
if (taskWorkResponse.stopReason === "cancelled") {
|
|
1214
|
+
throw new Error(errors.usage.agentPromptCancelled);
|
|
1215
|
+
}
|
|
1216
|
+
// Phase 2: Reflect (always sent after task-work completes)
|
|
1217
|
+
info("Sending reflect prompt to agent...");
|
|
1218
|
+
await appendEvent(specDir, {
|
|
1219
|
+
session_id: sessionId,
|
|
1220
|
+
type: "prompt.sent",
|
|
1221
|
+
data: {
|
|
1222
|
+
iteration,
|
|
1223
|
+
phase: "reflect",
|
|
1224
|
+
prompt: reflectPrompt,
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
const reflectResponse = await agent.client.prompt({
|
|
1228
|
+
sessionId: acpSessionId,
|
|
1229
|
+
prompt: [{ type: "text", text: reflectPrompt }],
|
|
1230
|
+
});
|
|
1231
|
+
// Log reflect completion
|
|
1232
|
+
await appendEvent(specDir, {
|
|
1233
|
+
session_id: sessionId,
|
|
1234
|
+
type: "session.update",
|
|
1235
|
+
data: {
|
|
1236
|
+
iteration,
|
|
1237
|
+
phase: "reflect",
|
|
1238
|
+
stopReason: reflectResponse.stopReason,
|
|
1239
|
+
completed: true,
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
if (reflectResponse.stopReason === "cancelled") {
|
|
401
1243
|
throw new Error(errors.usage.agentPromptCancelled);
|
|
402
1244
|
}
|
|
403
1245
|
succeeded = true;
|
|
@@ -420,6 +1262,31 @@ export function registerRalphCommand(program) {
|
|
|
420
1262
|
await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
|
|
421
1263
|
success(`Completed iteration ${iteration}`);
|
|
422
1264
|
consecutiveFailures = 0;
|
|
1265
|
+
// Track task refs from this iteration for wrap-up context
|
|
1266
|
+
for (const t of sessionCtx.active_tasks) {
|
|
1267
|
+
if (!recentTaskRefs.includes(t.ref)) {
|
|
1268
|
+
recentTaskRefs.push(t.ref);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
lastIterationCtx = sessionCtx;
|
|
1272
|
+
// AC: @ralph-end-loop ac-graceful - Check for end-loop signal
|
|
1273
|
+
if (endLoopRequested) {
|
|
1274
|
+
info("Agent requested end of loop. Exiting gracefully.");
|
|
1275
|
+
exitReason = "end_loop_signal";
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
// Periodic agent restart to prevent OOM
|
|
1279
|
+
// AC: @cli-ralph ac-restart-periodic
|
|
1280
|
+
if (restartEvery > 0 &&
|
|
1281
|
+
iteration % restartEvery === 0 &&
|
|
1282
|
+
iteration < maxLoops) {
|
|
1283
|
+
info(`Restarting agent to prevent memory buildup (every ${restartEvery} iterations)...`);
|
|
1284
|
+
if (agent) {
|
|
1285
|
+
agent.kill();
|
|
1286
|
+
agent = null;
|
|
1287
|
+
acpSessionId = null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
423
1290
|
}
|
|
424
1291
|
else {
|
|
425
1292
|
consecutiveFailures++;
|
|
@@ -427,34 +1294,99 @@ export function registerRalphCommand(program) {
|
|
|
427
1294
|
if (lastError) {
|
|
428
1295
|
error(errors.failures.lastError(lastError.message));
|
|
429
1296
|
}
|
|
1297
|
+
// AC: @loop-mode-error-handling - Track per-task failures
|
|
1298
|
+
const errorDesc = lastError?.message || "Iteration failed after retries";
|
|
1299
|
+
await handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDesc);
|
|
430
1300
|
if (consecutiveFailures >= maxFailures) {
|
|
431
1301
|
error(errors.failures.reachedMaxFailures(maxFailures));
|
|
1302
|
+
exitReason = "max_failures";
|
|
1303
|
+
lastErrorMessage = lastError?.message;
|
|
1304
|
+
lastIterationCtx = sessionCtx;
|
|
432
1305
|
break;
|
|
433
1306
|
}
|
|
434
|
-
info(
|
|
1307
|
+
info("Continuing to next iteration...");
|
|
435
1308
|
}
|
|
436
1309
|
}
|
|
1310
|
+
// If loop completed all iterations without breaking
|
|
1311
|
+
if (exitReason === null) {
|
|
1312
|
+
exitReason = "max_iterations";
|
|
1313
|
+
}
|
|
437
1314
|
}
|
|
438
1315
|
finally {
|
|
1316
|
+
// Remove signal handlers to avoid double cleanup
|
|
1317
|
+
process.off("SIGINT", sigintHandler);
|
|
1318
|
+
process.off("SIGTERM", sigtermHandler);
|
|
439
1319
|
// Clean up agent
|
|
440
1320
|
if (agent) {
|
|
441
1321
|
agent.kill();
|
|
1322
|
+
agent = null;
|
|
1323
|
+
}
|
|
1324
|
+
// AC: @ralph-task-limit ac-reset - Clear marker file when session ends
|
|
1325
|
+
await clearTaskLimitMarker(ctx.rootDir);
|
|
1326
|
+
// AC: @ralph-end-loop ac-cleanup - Clear end-loop marker when session ends
|
|
1327
|
+
await clearEndLoopMarker(ctx.rootDir);
|
|
1328
|
+
// AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
|
|
1329
|
+
// Spawn wrap-up agent if not dry-run and we have an exit reason
|
|
1330
|
+
if (!options.dryRun && exitReason) {
|
|
1331
|
+
console.log("");
|
|
1332
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1333
|
+
console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Starting Wrap-Up`));
|
|
1334
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1335
|
+
console.log("");
|
|
1336
|
+
const inProgressTasks = lastIterationCtx?.active_tasks || [];
|
|
1337
|
+
const pendingReviewTasks = lastIterationCtx?.pending_review_tasks || [];
|
|
1338
|
+
const wrapUpCtx = buildWrapUpContext(exitReason, sessionId, maxLoops, // Use maxLoops as iteration (we're at the end)
|
|
1339
|
+
maxLoops, inProgressTasks, pendingReviewTasks, recentTaskRefs, process.cwd(), lastErrorMessage);
|
|
1340
|
+
info(`Exit reason: ${exitReason}`);
|
|
1341
|
+
info(`Working tree: ${wrapUpCtx.workingTree.clean ? "clean" : "has uncommitted changes"}`);
|
|
1342
|
+
const wrapUpResult = await runWrapUpAgent(adapter, wrapUpCtx, {
|
|
1343
|
+
yolo: options.yolo,
|
|
1344
|
+
cwd: process.cwd(),
|
|
1345
|
+
handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, options.yolo),
|
|
1346
|
+
}, DEFAULT_WRAPUP_TIMEOUT);
|
|
1347
|
+
// Log wrap-up result
|
|
1348
|
+
await appendEvent(specDir, {
|
|
1349
|
+
session_id: sessionId,
|
|
1350
|
+
type: "session.wrapup",
|
|
1351
|
+
data: {
|
|
1352
|
+
exitReason,
|
|
1353
|
+
result: wrapUpResult,
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
if (wrapUpResult.skipped) {
|
|
1357
|
+
info(`${WRAPUP_AGENT_PREFIX} Skipped: ${wrapUpResult.skipReason}`);
|
|
1358
|
+
}
|
|
1359
|
+
else if (wrapUpResult.timedOut) {
|
|
1360
|
+
warn(`${WRAPUP_AGENT_PREFIX} Timed out after ${DEFAULT_WRAPUP_TIMEOUT / 1000}s`);
|
|
1361
|
+
}
|
|
1362
|
+
else if (!wrapUpResult.success) {
|
|
1363
|
+
warn(`${WRAPUP_AGENT_PREFIX} Failed: ${wrapUpResult.error}`);
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
success(`${WRAPUP_AGENT_PREFIX} Completed`);
|
|
1367
|
+
}
|
|
1368
|
+
console.log("");
|
|
1369
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1370
|
+
console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Wrap-Up Complete`));
|
|
1371
|
+
console.log(chalk.cyan(`${"═".repeat(60)}`));
|
|
1372
|
+
console.log("");
|
|
442
1373
|
}
|
|
443
1374
|
// Log session end
|
|
444
|
-
const status = consecutiveFailures >= maxFailures ?
|
|
1375
|
+
const status = consecutiveFailures >= maxFailures ? "abandoned" : "completed";
|
|
445
1376
|
await appendEvent(specDir, {
|
|
446
1377
|
session_id: sessionId,
|
|
447
|
-
type:
|
|
1378
|
+
type: "session.end",
|
|
448
1379
|
data: {
|
|
449
1380
|
status,
|
|
450
1381
|
consecutiveFailures,
|
|
1382
|
+
exitReason,
|
|
451
1383
|
},
|
|
452
1384
|
});
|
|
453
1385
|
await updateSessionStatus(specDir, sessionId, status);
|
|
454
1386
|
}
|
|
455
|
-
console.log(chalk.green(`\n${
|
|
456
|
-
success(
|
|
457
|
-
console.log(chalk.green(`${
|
|
1387
|
+
console.log(chalk.green(`\n${"─".repeat(60)}`));
|
|
1388
|
+
success("Ralph loop completed");
|
|
1389
|
+
console.log(chalk.green(`${"─".repeat(60)}\n`));
|
|
458
1390
|
}
|
|
459
1391
|
catch (err) {
|
|
460
1392
|
error(errors.failures.ralphLoop, err);
|