@kynetic-ai/spec 0.1.2 → 0.4.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 +107 -0
- package/dist/cli/batch-exec.d.ts.map +1 -0
- package/dist/cli/batch-exec.js +706 -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 +141 -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 +58 -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 +534 -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 +547 -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 +1109 -170
- 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 +810 -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 +1267 -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 +56 -0
- package/dist/cli/commands/skill-install.d.ts.map +1 -0
- package/dist/cli/commands/skill-install.js +509 -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 +584 -350
- 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 +225 -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 +483 -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 +237 -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 +402 -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 +457 -0
- package/dist/parser/config.d.ts.map +1 -0
- package/dist/parser/config.js +373 -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 +197 -0
- package/dist/parser/plan-document.d.ts.map +1 -0
- package/dist/parser/plan-document.js +341 -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 +301 -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 +94 -0
- package/dist/ralph/subagent.d.ts.map +1 -0
- package/dist/ralph/subagent.js +193 -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 +97 -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 +8 -2
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +42 -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 +241 -1
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +810 -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 +55 -0
- package/dist/strings/errors.d.ts.map +1 -1
- package/dist/strings/errors.js +138 -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/triage/actions.d.ts +27 -0
- package/dist/triage/actions.d.ts.map +1 -0
- package/dist/triage/actions.js +95 -0
- package/dist/triage/actions.js.map +1 -0
- package/dist/triage/constants.d.ts +6 -0
- package/dist/triage/constants.d.ts.map +1 -0
- package/dist/triage/constants.js +7 -0
- package/dist/triage/constants.js.map +1 -0
- package/dist/triage/index.d.ts +3 -0
- package/dist/triage/index.d.ts.map +1 -0
- package/dist/triage/index.js +3 -0
- package/dist/triage/index.js.map +1 -0
- 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/create-workflow/SKILL.md +235 -0
- package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
- package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
- package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +193 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +303 -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/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
- package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
- package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +340 -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/create-workflow/SKILL.md +228 -0
- package/templates/skills/help/SKILL.md +37 -0
- package/templates/skills/manifest.yaml +60 -0
- package/templates/skills/observations/SKILL.md +137 -0
- package/templates/skills/plan/SKILL.md +336 -0
- package/templates/skills/reflect/SKILL.md +155 -0
- package/templates/skills/review/SKILL.md +186 -0
- package/templates/skills/task-work/SKILL.md +296 -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
- package/templates/skills/triage-automation/SKILL.md +134 -0
- package/templates/skills/triage-inbox/SKILL.md +225 -0
- package/templates/skills/writing-specs/SKILL.md +333 -0
package/dist/sessions/store.js
CHANGED
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
* session.yaml # Metadata
|
|
12
12
|
* events.jsonl # Append-only event log
|
|
13
13
|
*/
|
|
14
|
-
import * as fs from
|
|
15
|
-
import * as fsPromises from
|
|
16
|
-
import * as path from
|
|
17
|
-
import * as YAML from
|
|
18
|
-
import {
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as fsPromises from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as YAML from "yaml";
|
|
18
|
+
import { SessionEventSchema, SessionMetadataSchema, } from "./types.js";
|
|
19
19
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
20
|
-
const SESSIONS_DIR =
|
|
21
|
-
const METADATA_FILE =
|
|
22
|
-
const EVENTS_FILE =
|
|
20
|
+
const SESSIONS_DIR = "sessions";
|
|
21
|
+
const METADATA_FILE = "session.yaml";
|
|
22
|
+
const EVENTS_FILE = "events.jsonl";
|
|
23
23
|
// ─── Path Helpers ────────────────────────────────────────────────────────────
|
|
24
24
|
/**
|
|
25
25
|
* Get the sessions directory path within a spec directory.
|
|
@@ -72,14 +72,18 @@ export async function createSession(specDir, input) {
|
|
|
72
72
|
id: input.id,
|
|
73
73
|
task_id: input.task_id,
|
|
74
74
|
agent_type: input.agent_type,
|
|
75
|
-
status: input.status ??
|
|
75
|
+
status: input.status ?? "active",
|
|
76
76
|
started_at: input.started_at ?? new Date().toISOString(),
|
|
77
77
|
ended_at: undefined,
|
|
78
78
|
};
|
|
79
79
|
// Validate and write metadata
|
|
80
80
|
const validated = SessionMetadataSchema.parse(metadata);
|
|
81
|
-
const content = YAML.stringify(validated, {
|
|
82
|
-
|
|
81
|
+
const content = YAML.stringify(validated, {
|
|
82
|
+
indent: 2,
|
|
83
|
+
lineWidth: 100,
|
|
84
|
+
sortMapEntries: false,
|
|
85
|
+
});
|
|
86
|
+
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
83
87
|
return validated;
|
|
84
88
|
}
|
|
85
89
|
/**
|
|
@@ -92,7 +96,7 @@ export async function createSession(specDir, input) {
|
|
|
92
96
|
export async function getSession(specDir, sessionId) {
|
|
93
97
|
const metadataPath = getSessionMetadataPath(specDir, sessionId);
|
|
94
98
|
try {
|
|
95
|
-
const content = await fsPromises.readFile(metadataPath,
|
|
99
|
+
const content = await fsPromises.readFile(metadataPath, "utf-8");
|
|
96
100
|
const raw = YAML.parse(content);
|
|
97
101
|
return SessionMetadataSchema.parse(raw);
|
|
98
102
|
}
|
|
@@ -119,11 +123,15 @@ export async function updateSessionStatus(specDir, sessionId, status) {
|
|
|
119
123
|
const updated = {
|
|
120
124
|
...metadata,
|
|
121
125
|
status,
|
|
122
|
-
ended_at: status !==
|
|
126
|
+
ended_at: status !== "active" ? new Date().toISOString() : metadata.ended_at,
|
|
123
127
|
};
|
|
124
128
|
const metadataPath = getSessionMetadataPath(specDir, sessionId);
|
|
125
|
-
const content = YAML.stringify(updated, {
|
|
126
|
-
|
|
129
|
+
const content = YAML.stringify(updated, {
|
|
130
|
+
indent: 2,
|
|
131
|
+
lineWidth: 100,
|
|
132
|
+
sortMapEntries: false,
|
|
133
|
+
});
|
|
134
|
+
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
127
135
|
return updated;
|
|
128
136
|
}
|
|
129
137
|
/**
|
|
@@ -135,10 +143,10 @@ export async function updateSessionStatus(specDir, sessionId, status) {
|
|
|
135
143
|
export async function listSessions(specDir) {
|
|
136
144
|
const sessionsDir = getSessionsDir(specDir);
|
|
137
145
|
try {
|
|
138
|
-
const entries = await fsPromises.readdir(sessionsDir, {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
const entries = await fsPromises.readdir(sessionsDir, {
|
|
147
|
+
withFileTypes: true,
|
|
148
|
+
});
|
|
149
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
142
150
|
}
|
|
143
151
|
catch {
|
|
144
152
|
return [];
|
|
@@ -168,8 +176,11 @@ export async function sessionExists(specDir, sessionId) {
|
|
|
168
176
|
async function getEventCount(specDir, sessionId) {
|
|
169
177
|
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
170
178
|
try {
|
|
171
|
-
const content = await fsPromises.readFile(eventsPath,
|
|
172
|
-
const lines = content
|
|
179
|
+
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
180
|
+
const lines = content
|
|
181
|
+
.trim()
|
|
182
|
+
.split("\n")
|
|
183
|
+
.filter((line) => line.length > 0);
|
|
173
184
|
return lines.length;
|
|
174
185
|
}
|
|
175
186
|
catch {
|
|
@@ -200,7 +211,7 @@ export async function appendEvent(specDir, input) {
|
|
|
200
211
|
// Ensure session directory exists (lazy creation)
|
|
201
212
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
202
213
|
// Get current event count for seq assignment
|
|
203
|
-
const seq = input.seq ?? await getEventCount(specDir, input.session_id);
|
|
214
|
+
const seq = input.seq ?? (await getEventCount(specDir, input.session_id));
|
|
204
215
|
// Build full event
|
|
205
216
|
const event = {
|
|
206
217
|
ts: input.ts ?? Date.now(),
|
|
@@ -212,10 +223,10 @@ export async function appendEvent(specDir, input) {
|
|
|
212
223
|
};
|
|
213
224
|
// Validate event
|
|
214
225
|
const validated = SessionEventSchema.parse(event);
|
|
215
|
-
// AC-3
|
|
226
|
+
// AC: @session-events ac-3 - Use synchronous append for crash safety
|
|
216
227
|
// This ensures the line is fully written before returning
|
|
217
|
-
const line = JSON.stringify(validated)
|
|
218
|
-
fs.appendFileSync(eventsPath, line,
|
|
228
|
+
const line = `${JSON.stringify(validated)}\n`;
|
|
229
|
+
fs.appendFileSync(eventsPath, line, "utf-8");
|
|
219
230
|
return validated;
|
|
220
231
|
}
|
|
221
232
|
/**
|
|
@@ -230,8 +241,11 @@ export async function appendEvent(specDir, input) {
|
|
|
230
241
|
export async function readEvents(specDir, sessionId) {
|
|
231
242
|
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
232
243
|
try {
|
|
233
|
-
const content = await fsPromises.readFile(eventsPath,
|
|
234
|
-
const lines = content
|
|
244
|
+
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
245
|
+
const lines = content
|
|
246
|
+
.trim()
|
|
247
|
+
.split("\n")
|
|
248
|
+
.filter((line) => line.length > 0);
|
|
235
249
|
const events = [];
|
|
236
250
|
for (const line of lines) {
|
|
237
251
|
try {
|
|
@@ -243,13 +257,59 @@ export async function readEvents(specDir, sessionId) {
|
|
|
243
257
|
// Skip invalid lines
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
|
-
// AC-4
|
|
260
|
+
// AC: @session-events ac-4 - Sort by sequence number
|
|
247
261
|
return events.sort((a, b) => a.seq - b.seq);
|
|
248
262
|
}
|
|
249
263
|
catch {
|
|
250
264
|
return [];
|
|
251
265
|
}
|
|
252
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Deduplicate phased tool_call events.
|
|
269
|
+
*
|
|
270
|
+
* ACP SDK 0.14+ sends tool calls in two phases: first with empty rawInput,
|
|
271
|
+
* then with populated rawInput. This merges them by keeping only the version
|
|
272
|
+
* with populated rawInput per toolCallId.
|
|
273
|
+
*/
|
|
274
|
+
export function deduplicatePhasedToolCalls(events) {
|
|
275
|
+
// First pass: find toolCallIds that have a populated rawInput version
|
|
276
|
+
const populatedToolCalls = new Map(); // toolCallId → index
|
|
277
|
+
for (let i = 0; i < events.length; i++) {
|
|
278
|
+
const event = events[i];
|
|
279
|
+
if (event.type !== "session.update")
|
|
280
|
+
continue;
|
|
281
|
+
const data = event.data;
|
|
282
|
+
const update = data?.update;
|
|
283
|
+
if (update?.sessionUpdate !== "tool_call")
|
|
284
|
+
continue;
|
|
285
|
+
const toolCallId = update.toolCallId || update.tool_call_id || update.id;
|
|
286
|
+
if (!toolCallId)
|
|
287
|
+
continue;
|
|
288
|
+
const rawInput = update.rawInput;
|
|
289
|
+
const hasContent = rawInput && Object.keys(rawInput).length > 0;
|
|
290
|
+
if (hasContent) {
|
|
291
|
+
populatedToolCalls.set(toolCallId, i);
|
|
292
|
+
}
|
|
293
|
+
else if (!populatedToolCalls.has(toolCallId)) {
|
|
294
|
+
// First (empty) version - track it in case no populated version exists
|
|
295
|
+
populatedToolCalls.set(toolCallId, i);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Second pass: keep only the best version per toolCallId
|
|
299
|
+
return events.filter((event, i) => {
|
|
300
|
+
if (event.type !== "session.update")
|
|
301
|
+
return true;
|
|
302
|
+
const data = event.data;
|
|
303
|
+
const update = data?.update;
|
|
304
|
+
if (update?.sessionUpdate !== "tool_call")
|
|
305
|
+
return true;
|
|
306
|
+
const toolCallId = update.toolCallId || update.tool_call_id || update.id;
|
|
307
|
+
if (!toolCallId)
|
|
308
|
+
return true;
|
|
309
|
+
// Keep this event only if it's the best version (populated or only version)
|
|
310
|
+
return populatedToolCalls.get(toolCallId) === i;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
253
313
|
/**
|
|
254
314
|
* Read events within a time range.
|
|
255
315
|
*
|
|
@@ -261,7 +321,7 @@ export async function readEvents(specDir, sessionId) {
|
|
|
261
321
|
*/
|
|
262
322
|
export async function readEventsSince(specDir, sessionId, since, until) {
|
|
263
323
|
const events = await readEvents(specDir, sessionId);
|
|
264
|
-
return events.filter(e => {
|
|
324
|
+
return events.filter((e) => {
|
|
265
325
|
if (e.ts < since)
|
|
266
326
|
return false;
|
|
267
327
|
if (until !== undefined && e.ts > until)
|
|
@@ -283,6 +343,121 @@ export async function getLastEvent(specDir, sessionId) {
|
|
|
283
343
|
}
|
|
284
344
|
return events[events.length - 1];
|
|
285
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Count lines in events.jsonl without parsing JSON.
|
|
348
|
+
* Much faster than readEvents() for large files.
|
|
349
|
+
*/
|
|
350
|
+
async function countEventLines(specDir, sessionId) {
|
|
351
|
+
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
352
|
+
try {
|
|
353
|
+
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
354
|
+
if (!content.trim())
|
|
355
|
+
return 0;
|
|
356
|
+
return content.trim().split("\n").length;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Count context-iter-*.json files for a session (iteration count).
|
|
364
|
+
*/
|
|
365
|
+
async function countIterations(specDir, sessionId) {
|
|
366
|
+
const sessionDir = getSessionDir(specDir, sessionId);
|
|
367
|
+
try {
|
|
368
|
+
const entries = await fsPromises.readdir(sessionDir);
|
|
369
|
+
return entries.filter((e) => e.startsWith("context-iter-") && e.endsWith(".json")).length;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Count task completions by scanning events for tool calls that invoke
|
|
377
|
+
* `kspec task complete` or `npm run dev -- task complete`.
|
|
378
|
+
*
|
|
379
|
+
* Real sessions record task completions as session.update events with
|
|
380
|
+
* sessionUpdate: "tool_call" and rawInput.command containing the complete command.
|
|
381
|
+
* We use a fast substring check before JSON parsing for performance.
|
|
382
|
+
*/
|
|
383
|
+
async function countTaskCompletions(specDir, sessionId) {
|
|
384
|
+
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
385
|
+
try {
|
|
386
|
+
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
387
|
+
if (!content.trim())
|
|
388
|
+
return 0;
|
|
389
|
+
const lines = content.trim().split("\n");
|
|
390
|
+
let count = 0;
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
// Quick substring pre-filter: only parse lines that might contain task complete commands
|
|
393
|
+
if (!line.includes("task complete"))
|
|
394
|
+
continue;
|
|
395
|
+
// Must be a tool_call event (session.update with sessionUpdate: "tool_call")
|
|
396
|
+
if (!line.includes('"tool_call"'))
|
|
397
|
+
continue;
|
|
398
|
+
try {
|
|
399
|
+
const event = JSON.parse(line);
|
|
400
|
+
const command = event?.data?.update?.rawInput?.command;
|
|
401
|
+
if (typeof command === "string" && /\btask complete\b/.test(command)) {
|
|
402
|
+
count++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Skip unparseable lines
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return count;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get a summary of a single session for list display.
|
|
417
|
+
*
|
|
418
|
+
* Gathers metadata and computes metrics lazily (only parses what's needed).
|
|
419
|
+
*
|
|
420
|
+
* @param specDir - The .kspec directory path
|
|
421
|
+
* @param sessionId - Session ID
|
|
422
|
+
* @returns Session summary or null if session doesn't exist
|
|
423
|
+
*/
|
|
424
|
+
export async function getSessionLogSummary(specDir, sessionId) {
|
|
425
|
+
const metadata = await getSession(specDir, sessionId);
|
|
426
|
+
if (!metadata)
|
|
427
|
+
return null;
|
|
428
|
+
const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
|
|
429
|
+
countEventLines(specDir, sessionId),
|
|
430
|
+
countIterationsBoundaryAware(specDir, sessionId),
|
|
431
|
+
countTaskCompletions(specDir, sessionId),
|
|
432
|
+
]);
|
|
433
|
+
const startMs = new Date(metadata.started_at).getTime();
|
|
434
|
+
const endMs = metadata.ended_at
|
|
435
|
+
? new Date(metadata.ended_at).getTime()
|
|
436
|
+
: Date.now();
|
|
437
|
+
const durationMs = endMs - startMs;
|
|
438
|
+
return {
|
|
439
|
+
id: metadata.id,
|
|
440
|
+
status: metadata.status,
|
|
441
|
+
agent_type: metadata.agent_type,
|
|
442
|
+
started_at: metadata.started_at,
|
|
443
|
+
ended_at: metadata.ended_at,
|
|
444
|
+
duration_ms: durationMs,
|
|
445
|
+
event_count: eventCount,
|
|
446
|
+
iteration_count: iterationCount,
|
|
447
|
+
tasks_completed: tasksCompleted,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get summaries for all sessions.
|
|
452
|
+
*
|
|
453
|
+
* @param specDir - The .kspec directory path
|
|
454
|
+
* @returns Array of session summaries
|
|
455
|
+
*/
|
|
456
|
+
export async function getAllSessionLogSummaries(specDir) {
|
|
457
|
+
const sessionIds = await listSessions(specDir);
|
|
458
|
+
const summaries = await Promise.all(sessionIds.map((id) => getSessionLogSummary(specDir, id)));
|
|
459
|
+
return summaries.filter((s) => s !== null);
|
|
460
|
+
}
|
|
286
461
|
// ─── Context Snapshots ───────────────────────────────────────────────────────
|
|
287
462
|
/**
|
|
288
463
|
* Save session context snapshot for a given iteration.
|
|
@@ -302,7 +477,7 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
|
|
|
302
477
|
await fsPromises.mkdir(sessionDir, { recursive: true });
|
|
303
478
|
// Write context snapshot as pretty JSON
|
|
304
479
|
const content = JSON.stringify(context, null, 2);
|
|
305
|
-
await fsPromises.writeFile(contextPath, content,
|
|
480
|
+
await fsPromises.writeFile(contextPath, content, "utf-8");
|
|
306
481
|
}
|
|
307
482
|
/**
|
|
308
483
|
* Read session context snapshot for a given iteration.
|
|
@@ -315,11 +490,615 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
|
|
|
315
490
|
export async function readSessionContext(specDir, sessionId, iteration) {
|
|
316
491
|
const contextPath = getSessionContextPath(specDir, sessionId, iteration);
|
|
317
492
|
try {
|
|
318
|
-
const content = await fsPromises.readFile(contextPath,
|
|
493
|
+
const content = await fsPromises.readFile(contextPath, "utf-8");
|
|
319
494
|
return JSON.parse(content);
|
|
320
495
|
}
|
|
321
496
|
catch {
|
|
322
497
|
return null;
|
|
323
498
|
}
|
|
324
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Resolve a session ID or prefix to a full session ID.
|
|
502
|
+
*
|
|
503
|
+
* AC: @session-log-show ac-7, ac-8, ac-9
|
|
504
|
+
*
|
|
505
|
+
* @param specDir - The .kspec directory path
|
|
506
|
+
* @param idOrPrefix - Full session ID or prefix (e.g., first 8 chars)
|
|
507
|
+
* @returns Resolution result
|
|
508
|
+
*/
|
|
509
|
+
export async function resolveSessionId(specDir, idOrPrefix) {
|
|
510
|
+
const sessionIds = await listSessions(specDir);
|
|
511
|
+
// First, try exact match
|
|
512
|
+
if (sessionIds.includes(idOrPrefix)) {
|
|
513
|
+
return { ok: true, id: idOrPrefix };
|
|
514
|
+
}
|
|
515
|
+
// Try prefix match
|
|
516
|
+
const matches = sessionIds.filter((id) => id.startsWith(idOrPrefix));
|
|
517
|
+
if (matches.length === 0) {
|
|
518
|
+
return { ok: false, error: "not_found" };
|
|
519
|
+
}
|
|
520
|
+
if (matches.length === 1) {
|
|
521
|
+
return { ok: true, id: matches[0] };
|
|
522
|
+
}
|
|
523
|
+
// Ambiguous - multiple matches
|
|
524
|
+
return { ok: false, error: "ambiguous", matches };
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get iteration number from a context snapshot file.
|
|
528
|
+
*/
|
|
529
|
+
async function getIterationNumbers(specDir, sessionId) {
|
|
530
|
+
const sessionDir = getSessionDir(specDir, sessionId);
|
|
531
|
+
try {
|
|
532
|
+
const entries = await fsPromises.readdir(sessionDir);
|
|
533
|
+
const iterations = [];
|
|
534
|
+
for (const entry of entries) {
|
|
535
|
+
const match = entry.match(/^context-iter-(\d+)\.json$/);
|
|
536
|
+
if (match) {
|
|
537
|
+
iterations.push(parseInt(match[1], 10));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return iterations.sort((a, b) => a - b);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Extract task refs from task commands in event data.
|
|
548
|
+
*/
|
|
549
|
+
function extractTaskRef(command) {
|
|
550
|
+
// Match patterns like: kspec task start @ref, kspec task complete @ref
|
|
551
|
+
const match = command.match(/@[\w-]+/);
|
|
552
|
+
return match ? match[0] : null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Find iteration boundaries from prompt.sent events with phase "task-work".
|
|
556
|
+
*
|
|
557
|
+
* Ralph emits these synchronously at the start of each iteration, so their
|
|
558
|
+
* array positions are reliable even when concurrent fire-and-forget events
|
|
559
|
+
* produce duplicate seq numbers.
|
|
560
|
+
*
|
|
561
|
+
* Returns validated, monotonically increasing boundaries.
|
|
562
|
+
*/
|
|
563
|
+
function findIterationBoundaries(events) {
|
|
564
|
+
const raw = [];
|
|
565
|
+
for (let i = 0; i < events.length; i++) {
|
|
566
|
+
const event = events[i];
|
|
567
|
+
if (event.type !== "prompt.sent")
|
|
568
|
+
continue;
|
|
569
|
+
const data = event.data;
|
|
570
|
+
if (data?.phase !== "task-work" ||
|
|
571
|
+
typeof data?.iteration !== "number") {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
raw.push({ index: i, iteration: data.iteration });
|
|
575
|
+
}
|
|
576
|
+
// Validate: filter to monotonically increasing iteration numbers, deduplicate
|
|
577
|
+
const validated = [];
|
|
578
|
+
let lastIter = -Infinity;
|
|
579
|
+
for (const b of raw) {
|
|
580
|
+
if (b.iteration > lastIter) {
|
|
581
|
+
validated.push(b);
|
|
582
|
+
lastIter = b.iteration;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return validated;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Extract task start/complete refs from a slice of events.
|
|
589
|
+
*/
|
|
590
|
+
function extractTaskTransitions(events) {
|
|
591
|
+
const tasksStarted = [];
|
|
592
|
+
const tasksCompleted = [];
|
|
593
|
+
for (const event of events) {
|
|
594
|
+
if (event.type === "session.update") {
|
|
595
|
+
const data = event.data;
|
|
596
|
+
const command = data?.update?.rawInput?.command;
|
|
597
|
+
if (typeof command === "string") {
|
|
598
|
+
if (/\btask start\b/.test(command)) {
|
|
599
|
+
const ref = extractTaskRef(command);
|
|
600
|
+
if (ref)
|
|
601
|
+
tasksStarted.push(ref);
|
|
602
|
+
}
|
|
603
|
+
else if (/\btask complete\b/.test(command)) {
|
|
604
|
+
const ref = extractTaskRef(command);
|
|
605
|
+
if (ref)
|
|
606
|
+
tasksCompleted.push(ref);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return { tasksStarted, tasksCompleted };
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Legacy iteration grouping: groups events by their data.iteration field.
|
|
615
|
+
*
|
|
616
|
+
* Used as fallback for sessions that don't have prompt.sent boundary events
|
|
617
|
+
* with phase "task-work" (pre-boundary sessions or non-ralph sessions).
|
|
618
|
+
*
|
|
619
|
+
* AC: @session-log-show ac-2
|
|
620
|
+
*/
|
|
621
|
+
function legacyIterationGrouping(events, snapshotIterations) {
|
|
622
|
+
// Collect all iteration numbers from both snapshots and events
|
|
623
|
+
const allIterations = new Set(snapshotIterations);
|
|
624
|
+
for (const event of events) {
|
|
625
|
+
const data = event.data;
|
|
626
|
+
if (typeof data?.iteration === "number") {
|
|
627
|
+
allIterations.add(data.iteration);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// If no iterations found anywhere, synthesize iteration-0 only if events exist
|
|
631
|
+
if (allIterations.size === 0) {
|
|
632
|
+
if (events.length === 0) {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
const { tasksStarted, tasksCompleted } = extractTaskTransitions(events);
|
|
636
|
+
return [
|
|
637
|
+
{
|
|
638
|
+
iteration: 0,
|
|
639
|
+
event_count: events.length,
|
|
640
|
+
tasks_started: tasksStarted,
|
|
641
|
+
tasks_completed: tasksCompleted,
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
}
|
|
645
|
+
// Create buckets for all known iterations
|
|
646
|
+
const iterations = Array.from(allIterations).sort((a, b) => a - b);
|
|
647
|
+
const iterationMap = new Map();
|
|
648
|
+
for (const n of iterations) {
|
|
649
|
+
iterationMap.set(n, []);
|
|
650
|
+
}
|
|
651
|
+
for (const event of events) {
|
|
652
|
+
const data = event.data;
|
|
653
|
+
const iter = data?.iteration;
|
|
654
|
+
if (typeof iter === "number" && iterationMap.has(iter)) {
|
|
655
|
+
iterationMap.get(iter).push(event);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
const fallbackIter = iterationMap.has(0) ? 0 : iterations[0];
|
|
659
|
+
iterationMap.get(fallbackIter).push(event);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const summaries = [];
|
|
663
|
+
for (const [iterNum, iterEvents] of iterationMap) {
|
|
664
|
+
const { tasksStarted, tasksCompleted } = extractTaskTransitions(iterEvents);
|
|
665
|
+
summaries.push({
|
|
666
|
+
iteration: iterNum,
|
|
667
|
+
event_count: iterEvents.length,
|
|
668
|
+
tasks_started: tasksStarted,
|
|
669
|
+
tasks_completed: tasksCompleted,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return summaries.sort((a, b) => a.iteration - b.iteration);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Boundary-based iteration grouping: splits events by prompt.sent boundary
|
|
676
|
+
* positions (array indices) instead of trusting data.iteration fields.
|
|
677
|
+
*
|
|
678
|
+
* This is resilient to producer-side bugs where concurrent fire-and-forget
|
|
679
|
+
* event logging captures the wrong iteration number.
|
|
680
|
+
*
|
|
681
|
+
* AC: @session-log-show ac-10
|
|
682
|
+
*/
|
|
683
|
+
function boundaryIterationGrouping(events, boundaries) {
|
|
684
|
+
const summaries = [];
|
|
685
|
+
for (let b = 0; b < boundaries.length; b++) {
|
|
686
|
+
const startIdx = boundaries[b].index;
|
|
687
|
+
const endIdx = b + 1 < boundaries.length ? boundaries[b + 1].index : events.length;
|
|
688
|
+
const iterEvents = events.slice(startIdx, endIdx);
|
|
689
|
+
const { tasksStarted, tasksCompleted } = extractTaskTransitions(iterEvents);
|
|
690
|
+
summaries.push({
|
|
691
|
+
iteration: boundaries[b].iteration,
|
|
692
|
+
event_count: iterEvents.length,
|
|
693
|
+
tasks_started: tasksStarted,
|
|
694
|
+
tasks_completed: tasksCompleted,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
// Pre-boundary events (before the first prompt.sent) merge into first iteration
|
|
698
|
+
if (boundaries.length > 0 && boundaries[0].index > 0) {
|
|
699
|
+
const preBoundaryEvents = events.slice(0, boundaries[0].index);
|
|
700
|
+
const { tasksStarted, tasksCompleted } = extractTaskTransitions(preBoundaryEvents);
|
|
701
|
+
summaries[0].event_count += preBoundaryEvents.length;
|
|
702
|
+
summaries[0].tasks_started = [...tasksStarted, ...summaries[0].tasks_started];
|
|
703
|
+
summaries[0].tasks_completed = [...tasksCompleted, ...summaries[0].tasks_completed];
|
|
704
|
+
}
|
|
705
|
+
return summaries;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Compute per-iteration summaries from events.
|
|
709
|
+
*
|
|
710
|
+
* Uses prompt.sent boundary events (phase: "task-work") when available for
|
|
711
|
+
* accurate index-based grouping. Falls back to legacy data.iteration grouping
|
|
712
|
+
* for sessions without boundaries.
|
|
713
|
+
*
|
|
714
|
+
* AC: @session-log-show ac-2, ac-10
|
|
715
|
+
*/
|
|
716
|
+
async function computeIterationSummaries(specDir, sessionId) {
|
|
717
|
+
const events = await readEvents(specDir, sessionId);
|
|
718
|
+
const boundaries = findIterationBoundaries(events);
|
|
719
|
+
if (boundaries.length > 0) {
|
|
720
|
+
return boundaryIterationGrouping(events, boundaries);
|
|
721
|
+
}
|
|
722
|
+
// Legacy fallback: no prompt.sent boundaries with phase "task-work"
|
|
723
|
+
const snapshotIterations = await getIterationNumbers(specDir, sessionId);
|
|
724
|
+
return legacyIterationGrouping(events, snapshotIterations);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Count iterations using boundary-aware logic, without computing full summaries.
|
|
728
|
+
*
|
|
729
|
+
* For use in getSessionLogSummary() (session log list, session log stats) to
|
|
730
|
+
* ensure iteration_count agrees with session log show.
|
|
731
|
+
*
|
|
732
|
+
* Falls back to counting context-iter-*.json files when no boundaries exist.
|
|
733
|
+
*/
|
|
734
|
+
async function countIterationsBoundaryAware(specDir, sessionId) {
|
|
735
|
+
const events = await readEvents(specDir, sessionId);
|
|
736
|
+
const boundaries = findIterationBoundaries(events);
|
|
737
|
+
if (boundaries.length > 0) {
|
|
738
|
+
return boundaries.length;
|
|
739
|
+
}
|
|
740
|
+
// Legacy fallback: count from context snapshots and event data
|
|
741
|
+
const snapshotIterations = await getIterationNumbers(specDir, sessionId);
|
|
742
|
+
const allIterations = new Set(snapshotIterations);
|
|
743
|
+
for (const event of events) {
|
|
744
|
+
const data = event.data;
|
|
745
|
+
if (typeof data?.iteration === "number") {
|
|
746
|
+
allIterations.add(data.iteration);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return allIterations.size || (events.length > 0 ? 1 : 0);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Get full session detail for session log show.
|
|
753
|
+
*
|
|
754
|
+
* @param specDir - The .kspec directory path
|
|
755
|
+
* @param sessionId - Session ID (must be resolved first)
|
|
756
|
+
* @returns Session detail or null if not found
|
|
757
|
+
*/
|
|
758
|
+
export async function getSessionLogDetail(specDir, sessionId) {
|
|
759
|
+
const metadata = await getSession(specDir, sessionId);
|
|
760
|
+
if (!metadata)
|
|
761
|
+
return null;
|
|
762
|
+
const [eventCount, iterations] = await Promise.all([
|
|
763
|
+
countEventLines(specDir, sessionId),
|
|
764
|
+
computeIterationSummaries(specDir, sessionId),
|
|
765
|
+
]);
|
|
766
|
+
const startMs = new Date(metadata.started_at).getTime();
|
|
767
|
+
const endMs = metadata.ended_at
|
|
768
|
+
? new Date(metadata.ended_at).getTime()
|
|
769
|
+
: Date.now();
|
|
770
|
+
const durationMs = endMs - startMs;
|
|
771
|
+
return {
|
|
772
|
+
id: metadata.id,
|
|
773
|
+
status: metadata.status,
|
|
774
|
+
agent_type: metadata.agent_type,
|
|
775
|
+
task_id: metadata.task_id,
|
|
776
|
+
started_at: metadata.started_at,
|
|
777
|
+
ended_at: metadata.ended_at,
|
|
778
|
+
duration_ms: durationMs,
|
|
779
|
+
event_count: eventCount,
|
|
780
|
+
iteration_count: iterations.length,
|
|
781
|
+
iterations,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Compute aggregate statistics from session summaries.
|
|
786
|
+
*
|
|
787
|
+
* @param summaries - Array of session log summaries
|
|
788
|
+
* @returns Aggregate statistics
|
|
789
|
+
*/
|
|
790
|
+
export function computeSessionLogStats(summaries) {
|
|
791
|
+
if (summaries.length === 0) {
|
|
792
|
+
return {
|
|
793
|
+
total_sessions: 0,
|
|
794
|
+
total_events: 0,
|
|
795
|
+
total_iterations: 0,
|
|
796
|
+
total_tasks_completed: 0,
|
|
797
|
+
total_duration_ms: 0,
|
|
798
|
+
avg_duration_ms: 0,
|
|
799
|
+
avg_iterations_per_session: 0,
|
|
800
|
+
avg_tasks_per_session: 0,
|
|
801
|
+
status_breakdown: [],
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// Compute totals
|
|
805
|
+
let totalEvents = 0;
|
|
806
|
+
let totalIterations = 0;
|
|
807
|
+
let totalTasksCompleted = 0;
|
|
808
|
+
let totalDuration = 0;
|
|
809
|
+
const statusCounts = {
|
|
810
|
+
active: 0,
|
|
811
|
+
completed: 0,
|
|
812
|
+
abandoned: 0,
|
|
813
|
+
};
|
|
814
|
+
for (const s of summaries) {
|
|
815
|
+
totalEvents += s.event_count;
|
|
816
|
+
totalIterations += s.iteration_count;
|
|
817
|
+
totalTasksCompleted += s.tasks_completed;
|
|
818
|
+
totalDuration += s.duration_ms;
|
|
819
|
+
statusCounts[s.status] = (statusCounts[s.status] || 0) + 1;
|
|
820
|
+
}
|
|
821
|
+
const n = summaries.length;
|
|
822
|
+
// Build status breakdown
|
|
823
|
+
const statusBreakdown = [];
|
|
824
|
+
for (const status of ["completed", "active", "abandoned"]) {
|
|
825
|
+
const count = statusCounts[status] || 0;
|
|
826
|
+
if (count > 0) {
|
|
827
|
+
statusBreakdown.push({
|
|
828
|
+
status,
|
|
829
|
+
count,
|
|
830
|
+
percentage: Math.round((count / n) * 100 * 10) / 10, // 1 decimal place
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
total_sessions: n,
|
|
836
|
+
total_events: totalEvents,
|
|
837
|
+
total_iterations: totalIterations,
|
|
838
|
+
total_tasks_completed: totalTasksCompleted,
|
|
839
|
+
total_duration_ms: totalDuration,
|
|
840
|
+
avg_duration_ms: Math.round(totalDuration / n),
|
|
841
|
+
avg_iterations_per_session: Math.round((totalIterations / n) * 10) / 10,
|
|
842
|
+
avg_tasks_per_session: Math.round((totalTasksCompleted / n) * 10) / 10,
|
|
843
|
+
status_breakdown: statusBreakdown,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Count tool calls by scanning events.jsonl for tool_call events.
|
|
848
|
+
*
|
|
849
|
+
* This is relatively expensive as it parses all events, so only call
|
|
850
|
+
* when --tool-usage is requested.
|
|
851
|
+
*
|
|
852
|
+
* AC: @session-log-stats ac-6
|
|
853
|
+
*/
|
|
854
|
+
export async function computeToolUsageStats(specDir, sessionIds, limit = 10) {
|
|
855
|
+
const toolCounts = {};
|
|
856
|
+
let totalToolCalls = 0;
|
|
857
|
+
for (const sessionId of sessionIds) {
|
|
858
|
+
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
859
|
+
try {
|
|
860
|
+
const content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
861
|
+
if (!content.trim())
|
|
862
|
+
continue;
|
|
863
|
+
const lines = content.trim().split("\n");
|
|
864
|
+
// Track seen toolCallIds to deduplicate phased events
|
|
865
|
+
const seenToolCallIds = new Set();
|
|
866
|
+
for (const line of lines) {
|
|
867
|
+
// Quick pre-filter: only parse lines that might be tool_call events
|
|
868
|
+
if (!line.includes('"tool_call"'))
|
|
869
|
+
continue;
|
|
870
|
+
try {
|
|
871
|
+
const event = JSON.parse(line);
|
|
872
|
+
if (event?.type === "session.update") {
|
|
873
|
+
const update = event?.data?.update;
|
|
874
|
+
if (update?.sessionUpdate === "tool_call") {
|
|
875
|
+
// Deduplicate phased tool_call events by toolCallId
|
|
876
|
+
const toolCallId = update?.toolCallId || update?.tool_call_id || update?.id;
|
|
877
|
+
if (toolCallId && seenToolCallIds.has(toolCallId))
|
|
878
|
+
continue;
|
|
879
|
+
if (toolCallId)
|
|
880
|
+
seenToolCallIds.add(toolCallId);
|
|
881
|
+
const toolName = update?._meta?.claudeCode?.toolName || "unknown";
|
|
882
|
+
toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
|
|
883
|
+
totalToolCalls++;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
// Skip unparseable lines
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
// Skip sessions without events
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// Sort by count descending, take top N
|
|
897
|
+
const sorted = Object.entries(toolCounts)
|
|
898
|
+
.map(([tool_name, count]) => ({
|
|
899
|
+
tool_name,
|
|
900
|
+
count,
|
|
901
|
+
percentage: totalToolCalls > 0
|
|
902
|
+
? Math.round((count / totalToolCalls) * 100 * 10) / 10
|
|
903
|
+
: 0,
|
|
904
|
+
}))
|
|
905
|
+
.sort((a, b) => b.count - a.count)
|
|
906
|
+
.slice(0, limit);
|
|
907
|
+
return sorted;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Compute ISO-8601 week number and week-year in UTC.
|
|
911
|
+
*
|
|
912
|
+
* ISO-8601 rules:
|
|
913
|
+
* - Week 1 contains the first Thursday of the year
|
|
914
|
+
* - Week starts on Monday
|
|
915
|
+
* - Week-year may differ from calendar year at year boundaries
|
|
916
|
+
*
|
|
917
|
+
* Returns [weekYear, weekNumber] tuple.
|
|
918
|
+
*/
|
|
919
|
+
function getISOWeekUTC(date) {
|
|
920
|
+
// Clone and normalize to UTC midnight
|
|
921
|
+
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
922
|
+
// Get day of week (ISO: Monday=1, Sunday=7)
|
|
923
|
+
const dayOfWeek = d.getUTCDay() || 7;
|
|
924
|
+
// Set to nearest Thursday (determines week-year)
|
|
925
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayOfWeek);
|
|
926
|
+
// Week-year is the year of this Thursday
|
|
927
|
+
const weekYear = d.getUTCFullYear();
|
|
928
|
+
// January 4 is always in week 1 (as it's in the first week with a Thursday)
|
|
929
|
+
const jan4 = new Date(Date.UTC(weekYear, 0, 4));
|
|
930
|
+
const jan4DayOfWeek = jan4.getUTCDay() || 7;
|
|
931
|
+
// Start of week 1 (Monday before or on Jan 4)
|
|
932
|
+
const week1Start = new Date(jan4);
|
|
933
|
+
week1Start.setUTCDate(jan4.getUTCDate() - (jan4DayOfWeek - 1));
|
|
934
|
+
// Week number = weeks between week 1 start and the Thursday we found
|
|
935
|
+
const weekNum = Math.floor((d.getTime() - week1Start.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1;
|
|
936
|
+
return [weekYear, weekNum];
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Group sessions by time period (day or week).
|
|
940
|
+
*
|
|
941
|
+
* AC: @session-log-stats ac-7
|
|
942
|
+
*/
|
|
943
|
+
export function computeTimePeriodStats(summaries, groupBy) {
|
|
944
|
+
const buckets = {};
|
|
945
|
+
for (const s of summaries) {
|
|
946
|
+
const date = new Date(s.started_at);
|
|
947
|
+
let period;
|
|
948
|
+
if (groupBy === "day") {
|
|
949
|
+
period = date.toISOString().split("T")[0]; // YYYY-MM-DD in UTC
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
// Week: use ISO-8601 week format (YYYY-Www) in UTC
|
|
953
|
+
const [weekYear, weekNum] = getISOWeekUTC(date);
|
|
954
|
+
period = `${weekYear}-W${weekNum.toString().padStart(2, "0")}`;
|
|
955
|
+
}
|
|
956
|
+
if (!buckets[period]) {
|
|
957
|
+
buckets[period] = { sessions: 0, tasks: 0, duration: 0 };
|
|
958
|
+
}
|
|
959
|
+
buckets[period].sessions += 1;
|
|
960
|
+
buckets[period].tasks += s.tasks_completed;
|
|
961
|
+
buckets[period].duration += s.duration_ms;
|
|
962
|
+
}
|
|
963
|
+
// Convert to array and sort by period (newest first for display)
|
|
964
|
+
const result = Object.entries(buckets)
|
|
965
|
+
.map(([period, data]) => ({
|
|
966
|
+
period,
|
|
967
|
+
sessions_count: data.sessions,
|
|
968
|
+
tasks_completed: data.tasks,
|
|
969
|
+
total_duration_ms: data.duration,
|
|
970
|
+
}))
|
|
971
|
+
.sort((a, b) => b.period.localeCompare(a.period));
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Extract a content excerpt around a match, limited to maxLength chars.
|
|
976
|
+
*
|
|
977
|
+
* AC: @session-log-search ac-4
|
|
978
|
+
*/
|
|
979
|
+
function extractContentExcerpt(data, pattern, maxLength = 200) {
|
|
980
|
+
// Stringify the data for searching
|
|
981
|
+
const str = JSON.stringify(data);
|
|
982
|
+
const lowerStr = str.toLowerCase();
|
|
983
|
+
const lowerPattern = pattern.toLowerCase();
|
|
984
|
+
const matchIndex = lowerStr.indexOf(lowerPattern);
|
|
985
|
+
if (matchIndex === -1) {
|
|
986
|
+
// Shouldn't happen since we pre-filtered, but return start of content
|
|
987
|
+
return str.length > maxLength ? str.slice(0, maxLength - 3) + "..." : str;
|
|
988
|
+
}
|
|
989
|
+
// Calculate excerpt window centered on match
|
|
990
|
+
const matchLen = pattern.length;
|
|
991
|
+
const contextBefore = Math.floor((maxLength - matchLen) / 2);
|
|
992
|
+
const start = Math.max(0, matchIndex - contextBefore);
|
|
993
|
+
const end = Math.min(str.length, start + maxLength);
|
|
994
|
+
let excerpt = str.slice(start, end);
|
|
995
|
+
// Add ellipsis indicators
|
|
996
|
+
if (start > 0) {
|
|
997
|
+
excerpt = "..." + excerpt.slice(3);
|
|
998
|
+
}
|
|
999
|
+
if (end < str.length) {
|
|
1000
|
+
excerpt = excerpt.slice(0, -3) + "...";
|
|
1001
|
+
}
|
|
1002
|
+
return excerpt;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Search across session events for a pattern.
|
|
1006
|
+
*
|
|
1007
|
+
* This streams through events.jsonl files, applying filters as early as possible
|
|
1008
|
+
* for performance. Sessions are pre-filtered by metadata (--since, --agent) before
|
|
1009
|
+
* scanning events to reduce I/O.
|
|
1010
|
+
*
|
|
1011
|
+
* AC: @session-log-search ac-1, ac-2, ac-3, ac-5, ac-7
|
|
1012
|
+
*
|
|
1013
|
+
* @param specDir - The .kspec directory path
|
|
1014
|
+
* @param pattern - Case-insensitive substring to search for
|
|
1015
|
+
* @param options - Search filtering options
|
|
1016
|
+
* @returns Array of search results grouped by session
|
|
1017
|
+
*/
|
|
1018
|
+
export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
1019
|
+
// Defense-in-depth: normalize limit to a valid positive integer
|
|
1020
|
+
const rawLimit = options.limit ?? 50;
|
|
1021
|
+
const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
|
|
1022
|
+
const lowerPattern = pattern.toLowerCase();
|
|
1023
|
+
// Get all session summaries for metadata filtering
|
|
1024
|
+
const allSummaries = await getAllSessionLogSummaries(specDir);
|
|
1025
|
+
// AC: @session-log-search ac-3 - Pre-filter by --since
|
|
1026
|
+
let filteredSummaries = allSummaries;
|
|
1027
|
+
if (options.sinceDate) {
|
|
1028
|
+
filteredSummaries = filteredSummaries.filter((s) => new Date(s.started_at) >= options.sinceDate);
|
|
1029
|
+
}
|
|
1030
|
+
// AC: @session-log-search ac-7 - Pre-filter by --agent
|
|
1031
|
+
if (options.agentType) {
|
|
1032
|
+
filteredSummaries = filteredSummaries.filter((s) => s.agent_type === options.agentType);
|
|
1033
|
+
}
|
|
1034
|
+
const results = [];
|
|
1035
|
+
let totalMatches = 0;
|
|
1036
|
+
for (const summary of filteredSummaries) {
|
|
1037
|
+
if (totalMatches >= limit)
|
|
1038
|
+
break;
|
|
1039
|
+
const eventsPath = getSessionEventsPath(specDir, summary.id);
|
|
1040
|
+
let content;
|
|
1041
|
+
try {
|
|
1042
|
+
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
continue; // Skip sessions without events
|
|
1046
|
+
}
|
|
1047
|
+
if (!content.trim())
|
|
1048
|
+
continue;
|
|
1049
|
+
const matches = [];
|
|
1050
|
+
const lines = content.trim().split("\n");
|
|
1051
|
+
// Track seen tool_call IDs to skip phased duplicates
|
|
1052
|
+
const seenToolCallIds = new Set();
|
|
1053
|
+
for (const line of lines) {
|
|
1054
|
+
if (totalMatches >= limit)
|
|
1055
|
+
break;
|
|
1056
|
+
// Quick substring pre-filter before parsing JSON
|
|
1057
|
+
if (!line.toLowerCase().includes(lowerPattern))
|
|
1058
|
+
continue;
|
|
1059
|
+
try {
|
|
1060
|
+
const event = JSON.parse(line);
|
|
1061
|
+
// AC: @session-log-search ac-2 - Filter by event type
|
|
1062
|
+
if (options.eventType && event.type !== options.eventType)
|
|
1063
|
+
continue;
|
|
1064
|
+
// Deduplicate phased tool_call events
|
|
1065
|
+
if (event?.type === "session.update") {
|
|
1066
|
+
const update = event?.data?.update;
|
|
1067
|
+
if (update?.sessionUpdate === "tool_call") {
|
|
1068
|
+
const toolCallId = update?.toolCallId || update?.tool_call_id || update?.id;
|
|
1069
|
+
if (toolCallId) {
|
|
1070
|
+
if (seenToolCallIds.has(toolCallId))
|
|
1071
|
+
continue;
|
|
1072
|
+
seenToolCallIds.add(toolCallId);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// Verify match in stringified data (not just line, in case pattern appears in metadata)
|
|
1077
|
+
const dataStr = JSON.stringify(event.data);
|
|
1078
|
+
if (!dataStr.toLowerCase().includes(lowerPattern))
|
|
1079
|
+
continue;
|
|
1080
|
+
// AC: @session-log-search ac-4 - Create match with excerpt
|
|
1081
|
+
matches.push({
|
|
1082
|
+
session_id: summary.id,
|
|
1083
|
+
timestamp: event.ts,
|
|
1084
|
+
event_type: event.type,
|
|
1085
|
+
content_excerpt: extractContentExcerpt(event.data, pattern, 200),
|
|
1086
|
+
});
|
|
1087
|
+
totalMatches++;
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
// Skip unparseable lines
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (matches.length > 0) {
|
|
1094
|
+
results.push({
|
|
1095
|
+
session_id: summary.id,
|
|
1096
|
+
agent_type: summary.agent_type,
|
|
1097
|
+
started_at: summary.started_at,
|
|
1098
|
+
matches,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return results;
|
|
1103
|
+
}
|
|
325
1104
|
//# sourceMappingURL=store.js.map
|