harness-async 0.1.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/dist/dashboard/assets/index-TGNGdtwt.js +246 -0
  4. package/dist/dashboard/assets/index-f4TpA4iP.css +1 -0
  5. package/dist/dashboard/index.html +13 -0
  6. package/dist/src/adapters/claude-adapter.js +52 -0
  7. package/dist/src/adapters/codex-adapter.js +55 -0
  8. package/dist/src/adapters/index.js +14 -0
  9. package/dist/src/adapters/shared.js +74 -0
  10. package/dist/src/cli/commands/daemon.js +116 -0
  11. package/dist/src/cli/commands/doctor.js +50 -0
  12. package/dist/src/cli/commands/hook.js +188 -0
  13. package/dist/src/cli/commands/init.js +22 -0
  14. package/dist/src/cli/commands/run.js +129 -0
  15. package/dist/src/cli/commands/schedule.js +105 -0
  16. package/dist/src/cli/commands/task.js +188 -0
  17. package/dist/src/cli/index.js +23 -0
  18. package/dist/src/cli/utils/notify.js +32 -0
  19. package/dist/src/cli/utils/output.js +94 -0
  20. package/dist/src/core/daemon.js +375 -0
  21. package/dist/src/core/dag.js +80 -0
  22. package/dist/src/core/event-log.js +34 -0
  23. package/dist/src/core/lock.js +25 -0
  24. package/dist/src/core/run-manager.js +265 -0
  25. package/dist/src/core/run-orchestrator.js +193 -0
  26. package/dist/src/core/scheduler.js +106 -0
  27. package/dist/src/core/sessions.js +48 -0
  28. package/dist/src/core/store.js +225 -0
  29. package/dist/src/core/task-manager.js +375 -0
  30. package/dist/src/core/tmux.js +51 -0
  31. package/dist/src/daemon.js +35 -0
  32. package/dist/src/dashboard/routes.js +107 -0
  33. package/dist/src/dashboard/server.js +142 -0
  34. package/dist/src/dashboard/ws.js +75 -0
  35. package/dist/src/types/adapter.js +30 -0
  36. package/dist/src/types/index.js +87 -0
  37. package/package.json +65 -0
@@ -0,0 +1 @@
1
+ .react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:IBM Plex Sans,Segoe UI,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.table{display:table}.grid{display:grid}.max-h-60{max-height:15rem}.min-h-\[392px\]{min-height:392px}.min-h-\[420px\]{min-height:420px}.min-h-\[80vh\]{min-height:80vh}.min-h-screen{min-height:100vh}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[200px\]{min-width:200px}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.max-w-\[1600px\]{max-width:1600px}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.border-separate{border-collapse:separate}.border-spacing-y-2{--tw-border-spacing-y: .5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-2xl{border-radius:1rem}.rounded-\[20px\]{border-radius:20px}.rounded-\[22px\]{border-radius:22px}.rounded-\[24px\]{border-radius:24px}.rounded-\[28px\]{border-radius:28px}.rounded-\[32px\]{border-radius:32px}.rounded-full{border-radius:9999px}.rounded-l-2xl{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.rounded-r-2xl{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.border{border-width:1px}.border-dashed{border-style:dashed}.border-moss{--tw-border-opacity: 1;border-color:rgb(41 69 60 / var(--tw-border-opacity, 1))}.border-moss\/10{border-color:#29453c1a}.border-moss\/15{border-color:#29453c26}.border-white\/10{border-color:#ffffff1a}.bg-\[\#f7fbf8\]{--tw-bg-opacity: 1;background-color:rgb(247 251 248 / var(--tw-bg-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-brass\/15{background-color:#a8813626}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-mist{--tw-bg-opacity: 1;background-color:rgb(237 245 239 / var(--tw-bg-opacity, 1))}.bg-moss{--tw-bg-opacity: 1;background-color:rgb(41 69 60 / var(--tw-bg-opacity, 1))}.bg-moss\/10{background-color:#29453c1a}.bg-rose-100{--tw-bg-opacity: 1;background-color:rgb(255 228 230 / var(--tw-bg-opacity, 1))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/60{background-color:#fff9}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-\[linear-gradient\(180deg\,_rgba\(255\,255\,255\,0\.96\)\,_rgba\(237\,245\,239\,0\.86\)\)\]{background-image:linear-gradient(180deg,#fffffff5,#edf5efdb)}.p-2{padding:.5rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-display{font-family:Space Grotesk,IBM Plex Sans,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.24em\]{letter-spacing:.24em}.tracking-\[0\.26em\]{letter-spacing:.26em}.tracking-\[0\.28em\]{letter-spacing:.28em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-normal{letter-spacing:0em}.tracking-tight{letter-spacing:-.025em}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-ember{--tw-text-opacity: 1;color:rgb(139 58 31 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(17 33 29 / var(--tw-text-opacity, 1))}.text-mist{--tw-text-opacity: 1;color:rgb(237 245 239 / var(--tw-text-opacity, 1))}.text-mist\/70{color:#edf5efb3}.text-moss{--tw-text-opacity: 1;color:rgb(41 69 60 / var(--tw-text-opacity, 1))}.text-moss\/50{color:#29453c80}.text-moss\/55{color:#29453c8c}.text-moss\/60{color:#29453c99}.text-moss\/65{color:#29453ca6}.text-moss\/70{color:#29453cb3}.text-moss\/75{color:#29453cbf}.text-rose-700{--tw-text-opacity: 1;color:rgb(190 18 60 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.opacity-70{opacity:.7}.shadow-panel{--tw-shadow: 0 18px 42px rgba(17, 33, 29, .12);--tw-shadow-colored: 0 18px 42px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:IBM Plex Sans,Segoe UI,sans-serif;color:#11211d;background:radial-gradient(circle at top left,rgba(41,69,60,.08),transparent 32%),linear-gradient(180deg,#f7fbf8,#edf5ef)}body{margin:0;min-width:320px}*{box-sizing:border-box}.hover\:border-moss\/30:hover{border-color:#29453c4d}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1024px){.lg\:w-\[280px\]{width:280px}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:1280px){.xl\:grid-cols-\[minmax\(0\,1\.2fr\)_minmax\(360px\,0\.8fr\)\]{grid-template-columns:minmax(0,1.2fr) minmax(360px,.8fr)}.xl\:flex-row{flex-direction:row}.xl\:items-end{align-items:flex-end}.xl\:justify-between{justify-content:space-between}}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Harness Async Dashboard</title>
7
+ <script type="module" crossorigin src="/assets/index-TGNGdtwt.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-f4TpA4iP.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,52 @@
1
+ import { createTmuxRunner, createTmuxSession, killTmuxSession, } from '../core/tmux.js';
2
+ import { collectRunSnapshot, detectBinary, quoteShell, writeLaunchScript, } from './shared.js';
3
+ export class ClaudeAdapter {
4
+ options;
5
+ name = 'claude';
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ }
9
+ async detect() {
10
+ return detectBinary(this.options.binary ?? 'claude');
11
+ }
12
+ async prepare(task, context) {
13
+ const directory = task.project ?? context.cwd;
14
+ const command = [
15
+ quoteShell(this.options.binary ?? 'claude'),
16
+ '--add-dir',
17
+ quoteShell(directory),
18
+ '--allowedTools',
19
+ quoteShell('Bash(ha:*)'),
20
+ '"$(cat',
21
+ quoteShell(context.promptPath),
22
+ ')"',
23
+ ].join(' ');
24
+ return {
25
+ command,
26
+ directory,
27
+ };
28
+ }
29
+ async start(run, context) {
30
+ const prepared = await this.prepare(context.task, context);
31
+ const launchScript = await writeLaunchScript(run, prepared.command);
32
+ await createTmuxSession({
33
+ sessionName: run.tmuxSession,
34
+ cwd: prepared.directory,
35
+ command: launchScript,
36
+ logFile: run.stdoutPath,
37
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
38
+ });
39
+ }
40
+ async stop(run) {
41
+ await killTmuxSession(run.tmuxSession, {
42
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
43
+ cwd: run.directory,
44
+ });
45
+ }
46
+ async collect(run) {
47
+ return collectRunSnapshot(run, {
48
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
49
+ cwd: run.directory,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,55 @@
1
+ import { createTmuxRunner, createTmuxSession, killTmuxSession, } from '../core/tmux.js';
2
+ import { collectRunSnapshot, detectBinary, quoteShell, writeLaunchScript, } from './shared.js';
3
+ export class CodexAdapter {
4
+ options;
5
+ name = 'codex';
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ }
9
+ async detect() {
10
+ return detectBinary(this.options.binary ?? 'codex');
11
+ }
12
+ async prepare(task, context) {
13
+ const directory = task.project ?? context.cwd;
14
+ const command = [
15
+ quoteShell(this.options.binary ?? 'codex'),
16
+ '-C',
17
+ quoteShell(directory),
18
+ '--sandbox',
19
+ 'workspace-write',
20
+ '--ask-for-approval',
21
+ 'never',
22
+ '--no-alt-screen',
23
+ '"$(cat',
24
+ quoteShell(context.promptPath),
25
+ ')"',
26
+ ].join(' ');
27
+ return {
28
+ command,
29
+ directory,
30
+ };
31
+ }
32
+ async start(run, context) {
33
+ const prepared = await this.prepare(context.task, context);
34
+ const launchScript = await writeLaunchScript(run, prepared.command);
35
+ await createTmuxSession({
36
+ sessionName: run.tmuxSession,
37
+ cwd: prepared.directory,
38
+ command: launchScript,
39
+ logFile: run.stdoutPath,
40
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
41
+ });
42
+ }
43
+ async stop(run) {
44
+ await killTmuxSession(run.tmuxSession, {
45
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
46
+ cwd: run.directory,
47
+ });
48
+ }
49
+ async collect(run) {
50
+ return collectRunSnapshot(run, {
51
+ runTmux: this.options.runTmux ?? createTmuxRunner(this.options.tmuxBin),
52
+ cwd: run.directory,
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,14 @@
1
+ import { ClaudeAdapter } from './claude-adapter.js';
2
+ import { CodexAdapter } from './codex-adapter.js';
3
+ export function createAgentAdapter(tool, options = {}) {
4
+ if (tool === 'claude') {
5
+ return new ClaudeAdapter({
6
+ binary: options.claudeBin,
7
+ tmuxBin: options.tmuxBin,
8
+ });
9
+ }
10
+ return new CodexAdapter({
11
+ binary: options.codexBin,
12
+ tmuxBin: options.tmuxBin,
13
+ });
14
+ }
@@ -0,0 +1,74 @@
1
+ import { access, chmod, writeFile } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { getTmuxSessionOutput, isTmuxSessionAlive } from '../core/tmux.js';
6
+ import { readRunExitCode, readRunOutput } from '../core/run-manager.js';
7
+ const execFileAsync = promisify(execFile);
8
+ export async function detectBinary(binary, versionArgs = ['--version']) {
9
+ try {
10
+ await access(binary);
11
+ }
12
+ catch {
13
+ try {
14
+ const result = await execFileAsync('which', [binary]);
15
+ binary = result.stdout.trim();
16
+ }
17
+ catch {
18
+ return {
19
+ available: false,
20
+ supports: [],
21
+ version: null,
22
+ };
23
+ }
24
+ }
25
+ try {
26
+ const result = await execFileAsync(binary, versionArgs);
27
+ return {
28
+ available: true,
29
+ supports: ['sandbox'],
30
+ version: result.stdout.trim() || result.stderr.trim() || null,
31
+ };
32
+ }
33
+ catch {
34
+ return {
35
+ available: true,
36
+ supports: ['sandbox'],
37
+ version: null,
38
+ };
39
+ }
40
+ }
41
+ export async function writeLaunchScript(run, command) {
42
+ const filePath = join(run.runDir, 'launch.sh');
43
+ const script = `#!/bin/sh
44
+ set -u
45
+ ${command}
46
+ status=$?
47
+ printf '%s' "$status" > ${quoteShell(run.exitCodePath)}
48
+ exit "$status"
49
+ `;
50
+ await writeFile(filePath, script, 'utf8');
51
+ await chmod(filePath, 0o755);
52
+ return filePath;
53
+ }
54
+ export async function collectRunSnapshot(run, options = {}) {
55
+ const active = await isTmuxSessionAlive(run.tmuxSession, options);
56
+ const exitCode = await readRunExitCode(run);
57
+ let output = await readRunOutput(run);
58
+ if (output.trim().length === 0) {
59
+ try {
60
+ output = await getTmuxSessionOutput(run.tmuxSession, options);
61
+ }
62
+ catch {
63
+ output = '';
64
+ }
65
+ }
66
+ return {
67
+ active,
68
+ exitCode,
69
+ output,
70
+ };
71
+ }
72
+ export function quoteShell(value) {
73
+ return `'${value.replace(/'/g, `'\\''`)}'`;
74
+ }
@@ -0,0 +1,116 @@
1
+ import { getDaemonStatus, readDaemonLogs, startDockerDaemon, startLaunchdDaemon, stopDockerDaemon, stopLaunchdDaemon, } from '../../core/daemon.js';
2
+ import { initializeStore } from '../../core/store.js';
3
+ import { renderTable } from '../utils/output.js';
4
+ export function registerDaemonCommand(program) {
5
+ const daemon = program
6
+ .command('daemon')
7
+ .description('Manage the ha background daemon');
8
+ daemon
9
+ .command('start')
10
+ .description('Start the launchd-managed daemon')
11
+ .option('--docker', 'Start the dockerized daemon')
12
+ .action(async (options) => {
13
+ try {
14
+ await initializeStore({
15
+ cwd: process.cwd(),
16
+ homeDir: process.env.HA_HOME,
17
+ scope: 'global',
18
+ });
19
+ if (options.docker) {
20
+ await startDockerDaemon({
21
+ cwd: process.cwd(),
22
+ homeDir: process.env.HA_HOME,
23
+ dockerBin: process.env.HA_DOCKER_BIN,
24
+ });
25
+ console.log('Docker daemon started');
26
+ return;
27
+ }
28
+ const paths = await startLaunchdDaemon({
29
+ cwd: process.cwd(),
30
+ homeDir: process.env.HA_HOME,
31
+ launchAgentsDir: process.env.HA_LAUNCH_AGENTS_DIR,
32
+ logsDir: process.env.HA_DAEMON_LOG_DIR,
33
+ launchctlBin: process.env.HA_LAUNCHCTL_BIN,
34
+ daemonEntry: process.env.HA_DAEMON_ENTRY,
35
+ nodePath: process.env.HA_NODE_BIN,
36
+ });
37
+ console.log(`Daemon started with plist ${paths.plistPath}`);
38
+ }
39
+ catch (error) {
40
+ process.exitCode = 1;
41
+ console.error(error.message);
42
+ }
43
+ });
44
+ daemon
45
+ .command('stop')
46
+ .description('Stop the launchd-managed daemon')
47
+ .option('--docker', 'Stop the dockerized daemon')
48
+ .action(async (options) => {
49
+ try {
50
+ if (options.docker) {
51
+ await stopDockerDaemon({
52
+ cwd: process.cwd(),
53
+ homeDir: process.env.HA_HOME,
54
+ dockerBin: process.env.HA_DOCKER_BIN,
55
+ });
56
+ console.log('Docker daemon stopped');
57
+ return;
58
+ }
59
+ const paths = await stopLaunchdDaemon({
60
+ cwd: process.cwd(),
61
+ homeDir: process.env.HA_HOME,
62
+ launchAgentsDir: process.env.HA_LAUNCH_AGENTS_DIR,
63
+ logsDir: process.env.HA_DAEMON_LOG_DIR,
64
+ launchctlBin: process.env.HA_LAUNCHCTL_BIN,
65
+ });
66
+ console.log(`Daemon stopped and removed ${paths.plistPath}`);
67
+ }
68
+ catch (error) {
69
+ process.exitCode = 1;
70
+ console.error(error.message);
71
+ }
72
+ });
73
+ daemon
74
+ .command('status')
75
+ .description('Show daemon launchd status')
76
+ .action(async () => {
77
+ try {
78
+ const status = await getDaemonStatus({
79
+ cwd: process.cwd(),
80
+ homeDir: process.env.HA_HOME,
81
+ launchAgentsDir: process.env.HA_LAUNCH_AGENTS_DIR,
82
+ logsDir: process.env.HA_DAEMON_LOG_DIR,
83
+ launchctlBin: process.env.HA_LAUNCHCTL_BIN,
84
+ });
85
+ console.log(renderTable([
86
+ {
87
+ name: 'daemon',
88
+ status: status.state,
89
+ detail: status.pid ? `pid=${status.pid}` : status.plistPath,
90
+ },
91
+ ]));
92
+ }
93
+ catch (error) {
94
+ process.exitCode = 1;
95
+ console.error(error.message);
96
+ }
97
+ });
98
+ daemon
99
+ .command('logs')
100
+ .description('Print daemon stdout and stderr logs')
101
+ .action(async () => {
102
+ try {
103
+ const logs = await readDaemonLogs({
104
+ cwd: process.cwd(),
105
+ homeDir: process.env.HA_HOME,
106
+ launchAgentsDir: process.env.HA_LAUNCH_AGENTS_DIR,
107
+ logsDir: process.env.HA_DAEMON_LOG_DIR,
108
+ });
109
+ console.log(['stdout:', logs.stdout || '(empty)', '', 'stderr:', logs.stderr || '(empty)'].join('\n'));
110
+ }
111
+ catch (error) {
112
+ process.exitCode = 1;
113
+ console.error(error.message);
114
+ }
115
+ });
116
+ }
@@ -0,0 +1,50 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { renderTable } from '../utils/output.js';
5
+ export function registerDoctorCommand(program) {
6
+ program
7
+ .command('doctor')
8
+ .description('Inspect local environment readiness for ha')
9
+ .addHelpText('after', '\nExamples:\n $ ha doctor')
10
+ .action(() => {
11
+ const projectStore = join(process.cwd(), '.ha');
12
+ const homeDir = process.env.HA_HOME ?? process.env.HOME ?? '';
13
+ const globalStore = join(homeDir, '.ha');
14
+ const rows = [
15
+ checkNodeVersion(),
16
+ checkBinary('tmux', 'tmux'),
17
+ checkBinary('claude', 'claude'),
18
+ checkBinary('codex', 'codex'),
19
+ checkStore('project store', projectStore, 'Run `ha init` in this project'),
20
+ checkStore('global store', globalStore, 'Run `ha init --global`'),
21
+ ];
22
+ console.log(renderTable(rows));
23
+ });
24
+ }
25
+ function checkNodeVersion() {
26
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
27
+ const ok = Number.isFinite(major) && major >= 20;
28
+ return {
29
+ name: 'Node.js',
30
+ status: ok ? '✅' : '❌',
31
+ detail: ok ? process.versions.node : `Expected >= 20, got ${process.versions.node}`,
32
+ };
33
+ }
34
+ function checkBinary(name, command) {
35
+ const result = spawnSync('which', [command], { encoding: 'utf8' });
36
+ const ok = result.status === 0;
37
+ return {
38
+ name,
39
+ status: ok ? '✅' : '❌',
40
+ detail: ok ? result.stdout.trim() : `${command} not found`,
41
+ };
42
+ }
43
+ function checkStore(name, path, hint) {
44
+ const ok = existsSync(path);
45
+ return {
46
+ name,
47
+ status: ok ? '✅' : '❌',
48
+ detail: ok ? path : `${hint} (${path})`,
49
+ };
50
+ }
@@ -0,0 +1,188 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { notifyTaskEvent } from '../utils/notify.js';
4
+ import { getRun, listRuns, updateRunStatus } from '../../core/run-manager.js';
5
+ import { syncRunState } from '../../core/run-orchestrator.js';
6
+ import { getTask, updateTaskStatus } from '../../core/task-manager.js';
7
+ import { getSession, removeSession, upsertSession } from '../../core/sessions.js';
8
+ export function registerHookCommand(program) {
9
+ const hook = program.command('hook').description('Manage Claude/Codex hook integration');
10
+ hook
11
+ .command('post-run-start')
12
+ .description('Track a newly started run against the current agent session')
13
+ .action(async () => {
14
+ try {
15
+ const payload = await readHookPayload();
16
+ const sessionId = extractSessionId(payload);
17
+ if (!sessionId) {
18
+ throw new Error('Hook payload is missing session_id');
19
+ }
20
+ const [latestRun] = await listRuns({
21
+ cwd: process.cwd(),
22
+ homeDir: process.env.HA_HOME,
23
+ scope: 'project',
24
+ });
25
+ if (!latestRun) {
26
+ throw new Error('No run found to bind to this session');
27
+ }
28
+ await upsertSession({
29
+ cwd: process.cwd(),
30
+ homeDir: process.env.HA_HOME,
31
+ }, sessionId, {
32
+ taskId: latestRun.taskId,
33
+ runId: latestRun.id,
34
+ startedAt: latestRun.startedAt,
35
+ });
36
+ console.log(`Tracked session ${sessionId} -> run ${latestRun.id}`);
37
+ }
38
+ catch (error) {
39
+ process.exitCode = 1;
40
+ console.error(error.message);
41
+ }
42
+ });
43
+ for (const name of ['session-end', 'stop']) {
44
+ hook
45
+ .command(name)
46
+ .description(`Handle the ${name} lifecycle event`)
47
+ .action(async () => {
48
+ try {
49
+ const payload = await readHookPayload();
50
+ const sessionId = extractSessionId(payload);
51
+ if (!sessionId) {
52
+ throw new Error('Hook payload is missing session_id');
53
+ }
54
+ await handleSessionShutdown(sessionId);
55
+ }
56
+ catch (error) {
57
+ process.exitCode = 1;
58
+ console.error(error.message);
59
+ }
60
+ });
61
+ }
62
+ hook
63
+ .command('install')
64
+ .description('Install hook definitions into Claude project settings')
65
+ .requiredOption('--tool <tool>', 'Tool name, currently claude')
66
+ .action(async (options) => {
67
+ try {
68
+ if (options.tool !== 'claude') {
69
+ throw new Error('Only --tool claude is supported for hook installation');
70
+ }
71
+ const settingsPath = join(process.cwd(), '.claude', 'settings.json');
72
+ await mkdir(join(process.cwd(), '.claude'), { recursive: true });
73
+ const settings = await readJsonIfExists(settingsPath);
74
+ const hooks = (settings.hooks ??= {});
75
+ mergeHookEntry(hooks, 'PostToolUse', {
76
+ matcher: 'ha run start',
77
+ hooks: [{ type: 'command', command: 'ha hook post-run-start' }],
78
+ });
79
+ mergeHookEntry(hooks, 'SessionEnd', {
80
+ hooks: [{ type: 'command', command: 'ha hook session-end' }],
81
+ });
82
+ mergeHookEntry(hooks, 'Stop', {
83
+ hooks: [{ type: 'command', command: 'ha hook stop' }],
84
+ });
85
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
86
+ console.log(`Installed Claude hooks into ${settingsPath}`);
87
+ }
88
+ catch (error) {
89
+ process.exitCode = 1;
90
+ console.error(error.message);
91
+ }
92
+ });
93
+ }
94
+ async function handleSessionShutdown(sessionId) {
95
+ const context = {
96
+ cwd: process.cwd(),
97
+ homeDir: process.env.HA_HOME,
98
+ };
99
+ const session = await getSession(context, sessionId);
100
+ if (!session) {
101
+ console.log(`No tracked session for ${sessionId}`);
102
+ return;
103
+ }
104
+ const run = await syncRunState(context, session.runId).catch(async () => getRun({
105
+ ...context,
106
+ runId: session.runId,
107
+ scope: 'project',
108
+ }));
109
+ const task = await getTask({
110
+ ...context,
111
+ id: session.taskId,
112
+ scope: 'project',
113
+ });
114
+ if (['completed', 'failed', 'waiting-review'].includes(task.status)) {
115
+ await removeSession(context, sessionId);
116
+ console.log(`Session ${sessionId} already settled`);
117
+ return;
118
+ }
119
+ if (task.status === 'running') {
120
+ const now = new Date().toISOString();
121
+ await updateTaskStatus({
122
+ ...context,
123
+ id: task.id,
124
+ scope: 'project',
125
+ status: 'paused',
126
+ actor: 'system',
127
+ });
128
+ await updateRunStatus(run, 'paused', {
129
+ completedAt: now,
130
+ });
131
+ if (process.env.HA_NOTIFY_DISABLED !== '1') {
132
+ await notifyTaskEvent({
133
+ type: 'task.paused',
134
+ taskId: task.id,
135
+ title: task.title,
136
+ });
137
+ }
138
+ }
139
+ await removeSession(context, sessionId);
140
+ console.log(`Cleaned session ${sessionId}`);
141
+ }
142
+ async function readHookPayload() {
143
+ const chunks = [];
144
+ for await (const chunk of process.stdin) {
145
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
146
+ }
147
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
148
+ return raw.length > 0 ? JSON.parse(raw) : {};
149
+ }
150
+ function extractSessionId(payload) {
151
+ if (!payload || typeof payload !== 'object') {
152
+ return null;
153
+ }
154
+ const record = payload;
155
+ const direct = record.session_id ?? record.sessionId;
156
+ if (typeof direct === 'string' && direct.length > 0) {
157
+ return direct;
158
+ }
159
+ const nestedSession = record.session;
160
+ if (nestedSession && typeof nestedSession === 'object') {
161
+ const nestedRecord = nestedSession;
162
+ const nestedId = nestedRecord.id ?? nestedRecord.session_id;
163
+ if (typeof nestedId === 'string' && nestedId.length > 0) {
164
+ return nestedId;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ async function readJsonIfExists(filePath) {
170
+ try {
171
+ const raw = await readFile(filePath, 'utf8');
172
+ return JSON.parse(raw);
173
+ }
174
+ catch (error) {
175
+ if (error.code === 'ENOENT') {
176
+ return {};
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+ function mergeHookEntry(hooks, name, entry) {
182
+ const current = Array.isArray(hooks[name]) ? hooks[name] : [];
183
+ const serialized = JSON.stringify(entry);
184
+ if (!current.some((item) => JSON.stringify(item) === serialized)) {
185
+ current.push(entry);
186
+ }
187
+ hooks[name] = current;
188
+ }
@@ -0,0 +1,22 @@
1
+ import { initializeStore } from '../../core/store.js';
2
+ export function registerInitCommand(program) {
3
+ program
4
+ .command('init')
5
+ .description('Initialize project-level or global ha storage')
6
+ .option('--global', 'Initialize storage in ~/.ha')
7
+ .addHelpText('after', '\nExamples:\n $ ha init\n $ ha init --global')
8
+ .action(async (options) => {
9
+ const scope = options.global ? 'global' : 'project';
10
+ const result = await initializeStore({
11
+ cwd: process.cwd(),
12
+ homeDir: process.env.HA_HOME,
13
+ scope,
14
+ });
15
+ const targetLabel = scope === 'global' ? 'global store' : 'project store';
16
+ if (result.created) {
17
+ console.log(`Initialized ${targetLabel} at ${result.storeDir}`);
18
+ return;
19
+ }
20
+ console.log(`${targetLabel} already initialized at ${result.storeDir}`);
21
+ });
22
+ }