@kynetic-ai/spec 0.1.2 → 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 +1097 -169
- 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 +131 -19
- 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,108 +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 {
|
|
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
13
|
// Read version from package.json for ACP client info
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
|
-
const { version: packageVersion } = require(
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
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
|
+
}
|
|
25
267
|
// ─── Prompt Template ─────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
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
|
+
? `
|
|
29
272
|
## Session Focus (applies to ALL iterations)
|
|
30
273
|
|
|
31
274
|
> **${focus}**
|
|
32
275
|
|
|
33
276
|
Keep this focus in mind throughout your work. It takes priority over default task selection.
|
|
34
|
-
`
|
|
35
|
-
|
|
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
|
|
36
293
|
|
|
37
|
-
|
|
38
|
-
${
|
|
294
|
+
**Session ID:** \`${sessionId}\`
|
|
295
|
+
**Iteration:** ${iteration} of ${maxLoops}
|
|
296
|
+
**Mode:** Automated (no human in the loop)
|
|
297
|
+
${focusSection}${taskScopeSection}
|
|
39
298
|
|
|
40
299
|
## Current State
|
|
41
300
|
\`\`\`json
|
|
42
301
|
${JSON.stringify(sessionCtx, null, 2)}
|
|
43
302
|
\`\`\`
|
|
44
303
|
|
|
45
|
-
##
|
|
304
|
+
## Instructions
|
|
46
305
|
|
|
47
|
-
|
|
306
|
+
Run the task-work skill in loop mode:
|
|
48
307
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
\`\`\`
|
|
53
|
-
|
|
54
|
-
3. **Do the work**:
|
|
55
|
-
- Read relevant files to understand the task
|
|
56
|
-
- Make changes as needed
|
|
57
|
-
- Run tests if applicable
|
|
58
|
-
- Document as you go with task notes
|
|
308
|
+
\`\`\`
|
|
309
|
+
/task-work loop
|
|
310
|
+
\`\`\`
|
|
59
311
|
|
|
60
|
-
|
|
61
|
-
\`\`\`bash
|
|
62
|
-
kspec task note @task-ref "What you did, decisions made, etc."
|
|
63
|
-
\`\`\`
|
|
312
|
+
${modeDescription}
|
|
64
313
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
\`\`\`bash
|
|
68
|
-
kspec task submit @task-ref
|
|
69
|
-
\`\`\`
|
|
70
|
-
- If task is NOT done (WIP):
|
|
71
|
-
\`\`\`bash
|
|
72
|
-
kspec task note @task-ref "WIP: What's done, what remains..."
|
|
73
|
-
\`\`\`
|
|
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.
|
|
74
316
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
328
|
|
|
79
|
-
|
|
80
|
-
|
|
329
|
+
**Session ID:** \`${sessionId}\`
|
|
330
|
+
**Iteration:** ${iteration} of ${maxLoops}
|
|
331
|
+
**Phase:** Post-task reflection
|
|
81
332
|
|
|
82
|
-
|
|
83
|
-
Think about what you learned, any friction points, or patterns worth remembering.
|
|
333
|
+
## Instructions
|
|
84
334
|
|
|
85
|
-
|
|
86
|
-
\`\`\`bash
|
|
87
|
-
kspec meta observe friction "Description of systemic issue..."
|
|
88
|
-
kspec meta observe success "Pattern worth replicating..."
|
|
89
|
-
\`\`\`
|
|
335
|
+
Run the reflect skill in loop mode:
|
|
90
336
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
\`\`\`
|
|
337
|
+
\`\`\`
|
|
338
|
+
/reflect loop
|
|
339
|
+
\`\`\`
|
|
95
340
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
This is the last iteration of the loop. After completing your work:
|
|
105
|
-
1. Commit any remaining changes
|
|
106
|
-
2. Reflect on the overall session
|
|
107
|
-
3. Capture any final insights as observations
|
|
108
|
-
` : ''}`;
|
|
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
|
+
`;
|
|
109
349
|
}
|
|
110
350
|
// ─── Streaming Output ────────────────────────────────────────────────────────
|
|
111
351
|
// Translator and renderer are created per-session in the action handler.
|
|
@@ -121,9 +361,9 @@ This is the last iteration of the loop. After completing your work:
|
|
|
121
361
|
function validateAdapter(adapterPackage) {
|
|
122
362
|
// Use npx --no-install with --version to check if package exists
|
|
123
363
|
// This checks both global and local node_modules, handles scoped packages
|
|
124
|
-
const result = spawnSync(
|
|
125
|
-
encoding:
|
|
126
|
-
stdio:
|
|
364
|
+
const result = spawnSync("npx", ["--no-install", adapterPackage, "--version"], {
|
|
365
|
+
encoding: "utf-8",
|
|
366
|
+
stdio: "pipe",
|
|
127
367
|
});
|
|
128
368
|
if (result.status !== 0) {
|
|
129
369
|
error(`Adapter package not found: ${adapterPackage}. Install with: npm install -g ${adapterPackage}`);
|
|
@@ -136,48 +376,50 @@ function validateAdapter(adapterPackage) {
|
|
|
136
376
|
* Implements file operations, terminal commands, and permission handling.
|
|
137
377
|
*/
|
|
138
378
|
async function handleRequest(client, id, method, params, yolo) {
|
|
139
|
-
const p = params;
|
|
140
379
|
try {
|
|
141
380
|
switch (method) {
|
|
142
|
-
case
|
|
381
|
+
case "session/request_permission": {
|
|
382
|
+
const p = params;
|
|
143
383
|
// In yolo mode, auto-approve all permissions
|
|
144
384
|
// In normal mode, would need to implement permission UI
|
|
145
385
|
const options = p.options || [];
|
|
146
386
|
if (yolo) {
|
|
147
387
|
// Find an "allow" option (prefer allow_always, then allow_once)
|
|
148
|
-
const allowOption = options.find(o => o.kind ===
|
|
149
|
-
|
|
388
|
+
const allowOption = options.find((o) => o.kind === "allow_always") ||
|
|
389
|
+
options.find((o) => o.kind === "allow_once");
|
|
150
390
|
if (allowOption) {
|
|
151
391
|
client.respondPermission(id, {
|
|
152
|
-
outcome: { outcome:
|
|
392
|
+
outcome: { outcome: "selected", optionId: allowOption.optionId },
|
|
153
393
|
});
|
|
154
394
|
}
|
|
155
395
|
else {
|
|
156
396
|
// No allow option available - cancel
|
|
157
|
-
client.respondPermission(id, { outcome: { outcome:
|
|
397
|
+
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
158
398
|
}
|
|
159
399
|
}
|
|
160
400
|
else {
|
|
161
401
|
// TODO: Implement permission prompting
|
|
162
|
-
client.respondPermission(id, { outcome: { outcome:
|
|
402
|
+
client.respondPermission(id, { outcome: { outcome: "cancelled" } });
|
|
163
403
|
}
|
|
164
404
|
break;
|
|
165
405
|
}
|
|
166
|
-
case
|
|
167
|
-
const
|
|
168
|
-
const content = await fs.readFile(
|
|
169
|
-
client.
|
|
406
|
+
case "file/read": {
|
|
407
|
+
const p = params;
|
|
408
|
+
const content = await fs.readFile(p.path, "utf-8");
|
|
409
|
+
client.respondReadTextFile(id, { content });
|
|
170
410
|
break;
|
|
171
411
|
}
|
|
172
|
-
case
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
await fs.
|
|
176
|
-
|
|
177
|
-
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, {});
|
|
178
417
|
break;
|
|
179
418
|
}
|
|
180
|
-
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;
|
|
181
423
|
const command = p.command;
|
|
182
424
|
const cwd = p.cwd || process.cwd();
|
|
183
425
|
const timeout = p.timeout || 60000;
|
|
@@ -187,21 +429,22 @@ async function handleRequest(client, id, method, params, yolo) {
|
|
|
187
429
|
shell: true,
|
|
188
430
|
timeout,
|
|
189
431
|
});
|
|
190
|
-
let stdout =
|
|
191
|
-
let stderr =
|
|
192
|
-
child.stdout?.on(
|
|
432
|
+
let stdout = "";
|
|
433
|
+
let stderr = "";
|
|
434
|
+
child.stdout?.on("data", (data) => {
|
|
193
435
|
stdout += data.toString();
|
|
194
436
|
});
|
|
195
|
-
child.stderr?.on(
|
|
437
|
+
child.stderr?.on("data", (data) => {
|
|
196
438
|
stderr += data.toString();
|
|
197
439
|
});
|
|
198
|
-
child.on(
|
|
440
|
+
child.on("close", (code) => {
|
|
199
441
|
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
200
442
|
});
|
|
201
|
-
child.on(
|
|
443
|
+
child.on("error", (err) => {
|
|
202
444
|
resolve({ stdout, stderr: err.message, exitCode: 1 });
|
|
203
445
|
});
|
|
204
446
|
});
|
|
447
|
+
// Using generic respond() since this is a custom method
|
|
205
448
|
client.respond(id, result);
|
|
206
449
|
break;
|
|
207
450
|
}
|
|
@@ -215,65 +458,439 @@ async function handleRequest(client, id, method, params, yolo) {
|
|
|
215
458
|
client.respondError(id, -32000, message);
|
|
216
459
|
}
|
|
217
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
|
+
}
|
|
218
731
|
// ─── Command Registration ────────────────────────────────────────────────────
|
|
219
732
|
export function registerRalphCommand(program) {
|
|
220
|
-
program
|
|
221
|
-
.command(
|
|
222
|
-
.description(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
.
|
|
228
|
-
.option(
|
|
229
|
-
.option('--adapter <id>', 'Agent adapter to use', 'claude-code-acp')
|
|
230
|
-
.option('--adapter-cmd <cmd>', 'Custom adapter command (for testing)')
|
|
231
|
-
.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")
|
|
232
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
|
+
}
|
|
233
806
|
try {
|
|
234
807
|
const maxLoops = parseInt(options.maxLoops, 10);
|
|
235
808
|
const maxRetries = parseInt(options.maxRetries, 10);
|
|
236
809
|
const maxFailures = parseInt(options.maxFailures, 10);
|
|
237
|
-
if (isNaN(maxLoops) || maxLoops < 1) {
|
|
810
|
+
if (Number.isNaN(maxLoops) || maxLoops < 1) {
|
|
238
811
|
error(errors.usage.maxLoopsPositive);
|
|
239
812
|
process.exit(EXIT_CODES.ERROR);
|
|
240
813
|
}
|
|
241
|
-
if (isNaN(maxRetries) || maxRetries < 0) {
|
|
814
|
+
if (Number.isNaN(maxRetries) || maxRetries < 0) {
|
|
242
815
|
error(errors.usage.maxRetriesNonNegative);
|
|
243
816
|
process.exit(EXIT_CODES.ERROR);
|
|
244
817
|
}
|
|
245
|
-
if (isNaN(maxFailures) || maxFailures < 1) {
|
|
818
|
+
if (Number.isNaN(maxFailures) || maxFailures < 1) {
|
|
246
819
|
error(errors.usage.maxFailuresPositive);
|
|
247
820
|
process.exit(EXIT_CODES.ERROR);
|
|
248
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
|
+
}
|
|
249
838
|
// Handle custom adapter command for testing
|
|
250
839
|
if (options.adapterCmd) {
|
|
251
840
|
const parts = options.adapterCmd.split(/\s+/);
|
|
252
841
|
const customAdapter = {
|
|
253
842
|
command: parts[0],
|
|
254
843
|
args: parts.slice(1),
|
|
255
|
-
description:
|
|
844
|
+
description: "Custom adapter via --adapter-cmd",
|
|
256
845
|
};
|
|
257
|
-
registerAdapter(
|
|
258
|
-
options.adapter =
|
|
846
|
+
registerAdapter("custom", customAdapter);
|
|
847
|
+
options.adapter = "custom";
|
|
259
848
|
}
|
|
260
849
|
// Resolve adapter
|
|
261
850
|
const adapter = resolveAdapter(options.adapter);
|
|
262
851
|
// Validate adapter package exists before proceeding
|
|
263
|
-
// Skip validation for
|
|
264
|
-
|
|
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) {
|
|
265
865
|
validateAdapter(adapter.args[0]);
|
|
266
866
|
}
|
|
267
|
-
// Add yolo flag to adapter args if needed
|
|
268
|
-
if (options.yolo &&
|
|
269
|
-
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
|
+
}
|
|
270
886
|
}
|
|
271
|
-
|
|
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})`);
|
|
272
891
|
if (options.focus) {
|
|
273
892
|
info(`Focus: ${options.focus}`);
|
|
274
893
|
}
|
|
275
|
-
// Initialize kspec context
|
|
276
|
-
const ctx = await initContext();
|
|
277
894
|
const specDir = ctx.specDir;
|
|
278
895
|
// Create session for event tracking
|
|
279
896
|
const sessionId = ulid();
|
|
@@ -285,53 +902,181 @@ export function registerRalphCommand(program) {
|
|
|
285
902
|
// Log session start
|
|
286
903
|
await appendEvent(specDir, {
|
|
287
904
|
session_id: sessionId,
|
|
288
|
-
type:
|
|
905
|
+
type: "session.start",
|
|
289
906
|
data: {
|
|
290
907
|
adapter: options.adapter,
|
|
291
908
|
maxLoops,
|
|
292
909
|
maxRetries,
|
|
293
910
|
maxFailures,
|
|
911
|
+
maxTasks,
|
|
294
912
|
yolo: options.yolo,
|
|
295
913
|
focus: options.focus,
|
|
914
|
+
explicitTasks: explicitTaskScope?.refs,
|
|
296
915
|
},
|
|
297
916
|
});
|
|
298
917
|
let consecutiveFailures = 0;
|
|
299
918
|
let agent = null;
|
|
300
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);
|
|
301
942
|
// Create translator and renderer for this session
|
|
302
943
|
const translator = createTranslator();
|
|
303
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 = [];
|
|
304
957
|
try {
|
|
305
958
|
for (let iteration = 1; iteration <= maxLoops; iteration++) {
|
|
306
959
|
renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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;
|
|
313
1033
|
if (!hasActiveTasks && !hasReadyTasks) {
|
|
314
|
-
|
|
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;
|
|
315
1042
|
break;
|
|
316
1043
|
}
|
|
317
|
-
//
|
|
318
|
-
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
|
|
319
1052
|
if (options.dryRun) {
|
|
320
|
-
console.log(chalk.yellow(
|
|
321
|
-
console.log(
|
|
322
|
-
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 ==="));
|
|
323
1067
|
break;
|
|
324
1068
|
}
|
|
325
|
-
// Log prompt
|
|
1069
|
+
// Log task-work prompt
|
|
326
1070
|
await appendEvent(specDir, {
|
|
327
1071
|
session_id: sessionId,
|
|
328
|
-
type:
|
|
1072
|
+
type: "prompt.sent",
|
|
329
1073
|
data: {
|
|
330
1074
|
iteration,
|
|
331
|
-
|
|
1075
|
+
phase: "task-work",
|
|
1076
|
+
prompt: taskWorkPrompt,
|
|
332
1077
|
tasks: {
|
|
333
|
-
active:
|
|
334
|
-
ready:
|
|
1078
|
+
active: currentCtx.active_tasks.map((t) => t.ref),
|
|
1079
|
+
ready: currentCtx.ready_tasks.map((t) => t.ref),
|
|
335
1080
|
},
|
|
336
1081
|
},
|
|
337
1082
|
});
|
|
@@ -345,63 +1090,156 @@ export function registerRalphCommand(program) {
|
|
|
345
1090
|
try {
|
|
346
1091
|
// Spawn agent if not already running
|
|
347
1092
|
if (!agent) {
|
|
348
|
-
info(
|
|
1093
|
+
info("Spawning ACP agent...");
|
|
349
1094
|
agent = await spawnAndInitialize(adapter, {
|
|
350
1095
|
cwd: process.cwd(),
|
|
351
1096
|
clientOptions: {
|
|
352
1097
|
clientInfo: {
|
|
353
|
-
name:
|
|
1098
|
+
name: "kspec-ralph",
|
|
354
1099
|
version: packageVersion,
|
|
355
1100
|
},
|
|
1101
|
+
methodTimeouts: {
|
|
1102
|
+
"session/prompt": RALPH_PROMPT_TIMEOUT,
|
|
1103
|
+
"session/resume": RALPH_PROMPT_TIMEOUT,
|
|
1104
|
+
},
|
|
356
1105
|
},
|
|
357
1106
|
});
|
|
358
1107
|
// Set up streaming update handler with translator + renderer
|
|
359
|
-
agent.client.on(
|
|
1108
|
+
agent.client.on("update", (_sid, update) => {
|
|
360
1109
|
// Translate ACP event to RalphEvent and render
|
|
361
1110
|
const event = translator.translate(update);
|
|
362
1111
|
if (event) {
|
|
363
1112
|
renderer.render(event);
|
|
364
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
|
+
}
|
|
365
1172
|
// Log raw update event (async, non-blocking)
|
|
366
1173
|
appendEvent(specDir, {
|
|
367
1174
|
session_id: sessionId,
|
|
368
|
-
type:
|
|
1175
|
+
type: "session.update",
|
|
369
1176
|
data: { iteration, update },
|
|
370
1177
|
}).catch(() => {
|
|
371
1178
|
// Ignore logging errors during streaming
|
|
372
1179
|
});
|
|
373
1180
|
});
|
|
374
1181
|
// Set up tool request handler
|
|
375
|
-
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
|
|
376
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
|
|
377
1186
|
agent.client.respondError(reqId, -32000, err.message);
|
|
378
1187
|
});
|
|
379
1188
|
});
|
|
380
1189
|
}
|
|
381
1190
|
// Create fresh ACP session per iteration to keep context clean
|
|
382
|
-
info(
|
|
1191
|
+
info("Creating ACP session...");
|
|
383
1192
|
acpSessionId = await agent.client.newSession({
|
|
384
1193
|
cwd: process.cwd(),
|
|
385
1194
|
mcpServers: [], // No MCP servers for now
|
|
386
1195
|
});
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
1196
|
+
// Phase 1: Task Work
|
|
1197
|
+
info("Sending task-work prompt to agent...");
|
|
1198
|
+
const taskWorkResponse = await agent.client.prompt({
|
|
390
1199
|
sessionId: acpSessionId,
|
|
391
|
-
prompt: [{ type:
|
|
1200
|
+
prompt: [{ type: "text", text: taskWorkPrompt }],
|
|
392
1201
|
});
|
|
393
|
-
// Log completion
|
|
1202
|
+
// Log task-work completion
|
|
394
1203
|
await appendEvent(specDir, {
|
|
395
1204
|
session_id: sessionId,
|
|
396
|
-
type:
|
|
1205
|
+
type: "session.update",
|
|
397
1206
|
data: {
|
|
398
1207
|
iteration,
|
|
399
|
-
|
|
1208
|
+
phase: "task-work",
|
|
1209
|
+
stopReason: taskWorkResponse.stopReason,
|
|
400
1210
|
completed: true,
|
|
401
1211
|
},
|
|
402
1212
|
});
|
|
403
|
-
|
|
404
|
-
|
|
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") {
|
|
405
1243
|
throw new Error(errors.usage.agentPromptCancelled);
|
|
406
1244
|
}
|
|
407
1245
|
succeeded = true;
|
|
@@ -424,6 +1262,31 @@ export function registerRalphCommand(program) {
|
|
|
424
1262
|
await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
|
|
425
1263
|
success(`Completed iteration ${iteration}`);
|
|
426
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
|
+
}
|
|
427
1290
|
}
|
|
428
1291
|
else {
|
|
429
1292
|
consecutiveFailures++;
|
|
@@ -431,34 +1294,99 @@ export function registerRalphCommand(program) {
|
|
|
431
1294
|
if (lastError) {
|
|
432
1295
|
error(errors.failures.lastError(lastError.message));
|
|
433
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);
|
|
434
1300
|
if (consecutiveFailures >= maxFailures) {
|
|
435
1301
|
error(errors.failures.reachedMaxFailures(maxFailures));
|
|
1302
|
+
exitReason = "max_failures";
|
|
1303
|
+
lastErrorMessage = lastError?.message;
|
|
1304
|
+
lastIterationCtx = sessionCtx;
|
|
436
1305
|
break;
|
|
437
1306
|
}
|
|
438
|
-
info(
|
|
1307
|
+
info("Continuing to next iteration...");
|
|
439
1308
|
}
|
|
440
1309
|
}
|
|
1310
|
+
// If loop completed all iterations without breaking
|
|
1311
|
+
if (exitReason === null) {
|
|
1312
|
+
exitReason = "max_iterations";
|
|
1313
|
+
}
|
|
441
1314
|
}
|
|
442
1315
|
finally {
|
|
1316
|
+
// Remove signal handlers to avoid double cleanup
|
|
1317
|
+
process.off("SIGINT", sigintHandler);
|
|
1318
|
+
process.off("SIGTERM", sigtermHandler);
|
|
443
1319
|
// Clean up agent
|
|
444
1320
|
if (agent) {
|
|
445
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("");
|
|
446
1373
|
}
|
|
447
1374
|
// Log session end
|
|
448
|
-
const status = consecutiveFailures >= maxFailures ?
|
|
1375
|
+
const status = consecutiveFailures >= maxFailures ? "abandoned" : "completed";
|
|
449
1376
|
await appendEvent(specDir, {
|
|
450
1377
|
session_id: sessionId,
|
|
451
|
-
type:
|
|
1378
|
+
type: "session.end",
|
|
452
1379
|
data: {
|
|
453
1380
|
status,
|
|
454
1381
|
consecutiveFailures,
|
|
1382
|
+
exitReason,
|
|
455
1383
|
},
|
|
456
1384
|
});
|
|
457
1385
|
await updateSessionStatus(specDir, sessionId, status);
|
|
458
1386
|
}
|
|
459
|
-
console.log(chalk.green(`\n${
|
|
460
|
-
success(
|
|
461
|
-
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`));
|
|
462
1390
|
}
|
|
463
1391
|
catch (err) {
|
|
464
1392
|
error(errors.failures.ralphLoop, err);
|