@walwal-harness/cli 7.1.40 → 7.1.41

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 (69) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  11. package/apps/harness-dashboard/components/Scene.tsx +568 -35
  12. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  13. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  14. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  15. package/apps/harness-dashboard/lib/types.ts +22 -222
  16. package/apps/harness-dashboard/playwright.config.ts +26 -7
  17. package/assets/templates/AGENTS-ko.md.template +1 -1
  18. package/assets/templates/AGENTS.md.template +12 -2
  19. package/assets/templates/HARNESS.md +2 -0
  20. package/assets/templates/company-pipeline-manifest.json +5 -14
  21. package/assets/templates/worker-report.md.template +38 -0
  22. package/bin/init.js +2 -1
  23. package/commands/goal.md +4 -2
  24. package/commands/hot-fix.md +1 -1
  25. package/commands/submission.md +1 -1
  26. package/package.json +1 -1
  27. package/scripts/conductor-tick.sh +12 -0
  28. package/scripts/harness-agenda.sh +105 -0
  29. package/scripts/harness-company-block.sh +89 -0
  30. package/scripts/harness-company-complete.sh +17 -8
  31. package/scripts/harness-company-cycle.sh +54 -0
  32. package/scripts/harness-progress-set.sh +8 -2
  33. package/scripts/harness-stop.sh +76 -0
  34. package/scripts/harness-wake-control.sh +151 -0
  35. package/scripts/harness-wake.sh +5 -0
  36. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  37. package/apps/harness-dashboard/components/Header.tsx +0 -40
  38. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  39. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  40. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  41. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  42. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  43. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  44. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  45. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  46. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  47. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  48. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  49. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  50. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  51. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  52. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  53. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  54. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  55. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  56. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  57. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  58. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  59. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  60. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  61. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  62. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  63. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  64. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  65. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  66. package/apps/harness-dashboard/lib/iso.ts +0 -150
  67. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  68. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  69. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -0,0 +1,105 @@
1
+ #!/bin/bash
2
+ # harness-agenda.sh — shared executive agenda (안건) for the perpetual operating loop.
3
+ #
4
+ # The agenda is the meetup file every CXX co-writes. The Stop hook forces the
5
+ # CEO to adjudicate it: while items are open/decided the loop keeps running; when
6
+ # it is empty the CEO must run a status-briefing round (each CXX confirms its
7
+ # live deliverables still operate toward the goal and raises new items) before
8
+ # the loop may yield to the next hourly wake.
9
+ #
10
+ # Store: .harness/documents/<goal-rel>/agenda.json
11
+ # { items:[{id,raised_by,kind,title,evidence,status:open|decided|done,
12
+ # decision,decided_by,created_at,updated_at}],
13
+ # cycles:int, last_cycle_at, last_briefing_at }
14
+ #
15
+ # Usage:
16
+ # harness-agenda.sh <project-root> <goal-rel> raise <cxx> <kind> "<title>" ["<evidence>"]
17
+ # harness-agenda.sh <project-root> <goal-rel> decide <id> "<decision>" [<owner-cxx>]
18
+ # harness-agenda.sh <project-root> <goal-rel> close <id>
19
+ # harness-agenda.sh <project-root> <goal-rel> brief # stamp a briefing round
20
+ # harness-agenda.sh <project-root> <goal-rel> active-count # items not done
21
+ # harness-agenda.sh <project-root> <goal-rel> open-count # items needing CEO decision
22
+ # harness-agenda.sh <project-root> <goal-rel> list
23
+
24
+ set -uo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
+ PROJECT_ROOT="${1:-.}"
28
+ GOAL_REL="${2:-}"
29
+ CMD="${3:-}"
30
+ command -v jq >/dev/null 2>&1 || { echo "[agenda] jq required" >&2; exit 1; }
31
+ [ -n "$GOAL_REL" ] && [ -n "$CMD" ] || { echo "[agenda] usage: $0 <project-root> <goal-rel> <cmd> ..." >&2; exit 2; }
32
+
33
+ AGENDA_DIR="$PROJECT_ROOT/.harness/documents/$GOAL_REL"
34
+ AGENDA="$AGENDA_DIR/agenda.json"
35
+ mkdir -p "$AGENDA_DIR"
36
+ [ -f "$AGENDA" ] || echo '{"items":[],"cycles":0,"last_cycle_at":null,"last_briefing_at":null}' > "$AGENDA"
37
+
38
+ STRUCTURED_LIB="$SCRIPT_DIR/lib/harness-structured-log.sh"
39
+ [ -f "$STRUCTURED_LIB" ] && source "$STRUCTURED_LIB"
40
+
41
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
42
+
43
+ apply() { # jq-program ; reads from extra --arg pairs already in "$@"
44
+ local prog="$1"; shift
45
+ local tmp="$AGENDA.tmp.$$"
46
+ if jq "$@" "$prog" "$AGENDA" > "$tmp" 2>/dev/null; then
47
+ mv "$tmp" "$AGENDA"
48
+ else
49
+ rm -f "$tmp"; echo "[agenda] write failed" >&2; exit 1
50
+ fi
51
+ }
52
+
53
+ upsert_todo() { # owner kind title status id priority
54
+ declare -f harness_todo_upsert >/dev/null 2>&1 || return 0
55
+ local todo
56
+ todo=$(jq -nc \
57
+ --arg id "$5" --arg owner "$1" --arg kind "$2" --arg title "$3" \
58
+ --arg status "$4" --arg now "$NOW" --argjson priority "${6:-60}" '
59
+ {id:$id,owner:$owner,kind:$kind,title:$title,status:$status,priority:$priority,
60
+ command:"agenda",mission_path:null,required_artifacts:[],
61
+ created_at:$now,updated_at:$now,last_heartbeat_at:$now,blocked_reason:null}')
62
+ harness_todo_upsert "$PROJECT_ROOT" "$todo"
63
+ }
64
+
65
+ case "$CMD" in
66
+ raise)
67
+ CXX="${4:-ceo}"; KIND="${5:-other}"; TITLE="${6:-untitled}"; EVID="${7:-}"
68
+ N=$(jq '.items | length' "$AGENDA")
69
+ ID="A-$(date -u +%Y%m%dT%H%M%SZ)-$N"
70
+ apply '.items += [{id:$id,raised_by:$cxx,kind:$kind,title:$title,evidence:$evid,status:"open",decision:null,decided_by:null,created_at:$now,updated_at:$now}]' \
71
+ --arg id "$ID" --arg cxx "$CXX" --arg kind "$KIND" --arg title "$TITLE" --arg evid "$EVID" --arg now "$NOW"
72
+ upsert_todo "$CXX" "agenda" "[안건] $TITLE" "pending" "$ID" 70
73
+ echo "$ID"
74
+ ;;
75
+ decide)
76
+ ID="${4:-}"; DECISION="${5:-}"; OWNER="${6:-}"
77
+ [ -n "$ID" ] || { echo "[agenda] decide needs <id>" >&2; exit 2; }
78
+ apply '.items |= map(if .id==$id then .status="decided" | .decision=$decision | .decided_by="ceo" | .updated_at=$now else . end)' \
79
+ --arg id "$ID" --arg decision "$DECISION" --arg now "$NOW"
80
+ [ -n "$OWNER" ] && upsert_todo "$OWNER" "agenda" "[결정] $DECISION" "active" "$ID-do" 70
81
+ echo "decided $ID"
82
+ ;;
83
+ close)
84
+ ID="${4:-}"
85
+ [ -n "$ID" ] || { echo "[agenda] close needs <id>" >&2; exit 2; }
86
+ apply '.items |= map(if .id==$id then .status="done" | .updated_at=$now else . end)' \
87
+ --arg id "$ID" --arg now "$NOW"
88
+ echo "closed $ID"
89
+ ;;
90
+ brief)
91
+ apply '.last_briefing_at=$now' --arg now "$NOW"
92
+ echo "briefing stamped"
93
+ ;;
94
+ active-count)
95
+ jq '[.items[] | select(.status != "done")] | length' "$AGENDA"
96
+ ;;
97
+ open-count)
98
+ jq '[.items[] | select(.status == "open")] | length' "$AGENDA"
99
+ ;;
100
+ list)
101
+ jq -c '[.items[] | select(.status != "done")]' "$AGENDA"
102
+ ;;
103
+ *)
104
+ echo "[agenda] unknown command: $CMD" >&2; exit 2 ;;
105
+ esac
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # harness-company-block.sh — mark the current v7 company mission BLOCKED on
3
+ # external authority (the second legitimate stop condition; the first is
4
+ # harness-company-complete.sh).
5
+ #
6
+ # Usage:
7
+ # bash scripts/harness-company-block.sh <project-root> "<missing authority>"
8
+ #
9
+ # This is the explicit running -> blocked transition. The CEO runs it when the
10
+ # next action genuinely requires external authority the harness cannot infer or
11
+ # obtain (new credentials/secrets, payment approval, legal/business acceptance,
12
+ # unavailable production access, destructive data action, or a direct conflict
13
+ # with the Owner's stated direction). It records the named blocker, flips the
14
+ # runtime out of "running" so the Stop hook releases the turn, and marks the
15
+ # active mission-state lifecycle "blocked". It does NOT archive documents.
16
+
17
+ set -uo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
+ PROJECT_ROOT="${1:-.}"
21
+ REASON="${2:-awaiting external authority}"
22
+
23
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
24
+ TODOS="$PROJECT_ROOT/.harness/todos/state.json"
25
+ [ -f "$PROGRESS" ] || {
26
+ echo "[company-block] not found: $PROGRESS" >&2
27
+ exit 1
28
+ }
29
+ command -v jq >/dev/null 2>&1 || {
30
+ echo "[company-block] jq is required" >&2
31
+ exit 1
32
+ }
33
+
34
+ if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
35
+ '.company_state.state = "idle" |
36
+ .conductor.state = "blocked" |
37
+ .conductor.current_action = ("blocked:" + $reason) |
38
+ .conductor.blocked_at = (now | todate) |
39
+ .conductor.tracks = [] |
40
+ .conductor.rendezvous = null |
41
+ .conductor.fork_meeting_id = null |
42
+ .current_agent = null |
43
+ .next_agent = "none" |
44
+ .agent_status = "blocked" |
45
+ .owner_prompt.status = "awaiting-authority" |
46
+ .owner_prompt.blocked_reason = $reason |
47
+ .owner_prompt.blocked_at = (now | todate) |
48
+ del(.conductor.completed_at) |
49
+ del(.owner_prompt.completed_at) |
50
+ del(.company_state.completed_at)' \
51
+ --arg reason "$REASON"; then
52
+ echo "[company-block] FAILED: progress.json transition did not apply (reason=$REASON). Runtime is NOT marked blocked." >&2
53
+ exit 1
54
+ fi
55
+
56
+ if [ -f "$TODOS" ]; then
57
+ tmp="$(mktemp)"
58
+ if jq --arg reason "$REASON" '
59
+ .owners |= with_entries(
60
+ .value |= map(
61
+ if (.status == "active" or .status == "pending")
62
+ then .status = "blocked" | .blocked_reason = $reason | .updated_at = (now | todate)
63
+ else .
64
+ end
65
+ )
66
+ )
67
+ ' "$TODOS" > "$tmp" 2>/dev/null; then mv "$tmp" "$TODOS"; else rm -f "$tmp"; fi
68
+ fi
69
+
70
+ DOCS="$PROJECT_ROOT/.harness/documents"
71
+ if [ -d "$DOCS" ]; then
72
+ find "$DOCS" -name mission-state.json -type f -print0 | while IFS= read -r -d '' state; do
73
+ active="$(jq -r '.active // false' "$state" 2>/dev/null || echo false)"
74
+ lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$state" 2>/dev/null || echo unknown)"
75
+ case "$lifecycle" in
76
+ closed|cancelled|superseded|complete|completed|blocked) continue ;;
77
+ esac
78
+ if [ "$active" = "true" ]; then
79
+ tmp="$(mktemp)"
80
+ jq --arg r "$REASON" '.lifecycle = "blocked" | .active = false | .blocked_reason = $r | .blocked_at = (now | todate)' "$state" > "$tmp" && mv "$tmp" "$state"
81
+ fi
82
+ done
83
+ fi
84
+
85
+ if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then
86
+ node "$SCRIPT_DIR/harness-activity-record.js" "$PROJECT_ROOT" >/dev/null 2>&1 || true
87
+ fi
88
+
89
+ echo "[company-block] marked blocked: $REASON"
@@ -25,12 +25,12 @@ command -v jq >/dev/null 2>&1 || {
25
25
  exit 1
26
26
  }
27
27
 
28
- bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
28
+ if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
29
29
  '.company_state.state = "idle" |
30
30
  .company_state.active_workers = 0 |
31
31
  .company_state.completed_at = (now | todate) |
32
32
  .conductor.state = "completed" |
33
- .conductor.current_action = "complete:'"$REASON"'" |
33
+ .conductor.current_action = ("complete:" + $reason) |
34
34
  .conductor.completed_at = (now | todate) |
35
35
  .conductor.tracks = [] |
36
36
  .conductor.rendezvous = null |
@@ -39,15 +39,22 @@ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
39
39
  .next_agent = "none" |
40
40
  .agent_status = "completed" |
41
41
  .owner_prompt.status = "completed" |
42
- .owner_prompt.completed_at = (now | todate)'
42
+ .owner_prompt.completed_at = (now | todate) |
43
+ del(.owner_prompt.blocked_reason) |
44
+ del(.owner_prompt.blocked_at) |
45
+ del(.conductor.blocked_at)' \
46
+ --arg reason "$REASON"; then
47
+ echo "[company-complete] FAILED: progress.json transition did not apply (reason=$REASON). Runtime is NOT marked complete." >&2
48
+ exit 1
49
+ fi
43
50
 
44
51
  if [ -f "$TODOS" ]; then
45
52
  tmp="$(mktemp)"
46
53
  jq '
47
54
  .owners |= with_entries(
48
55
  .value |= map(
49
- if (.status == "active" or .status == "pending" or .status == "paused")
50
- then .status = "done" | .updated_at = (now | todate)
56
+ if (.status == "active" or .status == "pending" or .status == "paused" or .status == "blocked")
57
+ then .status = "done" | del(.blocked_reason) | .updated_at = (now | todate)
51
58
  else .
52
59
  end
53
60
  )
@@ -61,11 +68,13 @@ if [ -d "$DOCS" ]; then
61
68
  active="$(jq -r '.active // false' "$state" 2>/dev/null || echo false)"
62
69
  lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$state" 2>/dev/null || echo unknown)"
63
70
  case "$lifecycle" in
64
- closed|cancelled|superseded|complete|blocked) continue ;;
71
+ closed|cancelled|superseded|complete|completed) continue ;;
65
72
  esac
66
- if [ "$active" = "true" ]; then
73
+ # A mission left "blocked" (active=false) is resumed and completed here, so
74
+ # clear its blocker fields too — block -> complete must leave no stale state.
75
+ if [ "$active" = "true" ] || [ "$lifecycle" = "blocked" ]; then
67
76
  tmp="$(mktemp)"
68
- jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate)' "$state" > "$tmp" && mv "$tmp" "$state"
77
+ jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate) | del(.blocked_reason) | del(.blocked_at)' "$state" > "$tmp" && mv "$tmp" "$state"
69
78
  fi
70
79
  done
71
80
  fi
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # harness-company-cycle.sh — record an OPERATING heartbeat for a perpetual goal.
3
+ #
4
+ # This is the steady-state transition for a never-completing (Mode 1) goal: the
5
+ # CEO runs it at the end of an operating tick AFTER a status-briefing round
6
+ # surfaced no new agenda. It flips the runtime to "operating" (a non-terminal
7
+ # state the Stop hook treats as a clean yield to the next hourly wake), bumps the
8
+ # cycle counter, and records activity — it does NOT terminate the mission or the
9
+ # company loop. Finite goals use harness-company-complete.sh instead.
10
+ #
11
+ # Usage:
12
+ # bash scripts/harness-company-cycle.sh <project-root> <goal-rel> [reason]
13
+
14
+ set -uo pipefail
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+ PROJECT_ROOT="${1:-.}"
18
+ GOAL_REL="${2:-}"
19
+ REASON="${3:-operating-heartbeat}"
20
+
21
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
22
+ [ -f "$PROGRESS" ] || { echo "[company-cycle] not found: $PROGRESS" >&2; exit 1; }
23
+ command -v jq >/dev/null 2>&1 || { echo "[company-cycle] jq required" >&2; exit 1; }
24
+
25
+ if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
26
+ '.company_state.state = "operating" |
27
+ .conductor.state = "operating" |
28
+ .conductor.current_action = ("operating:" + $reason) |
29
+ .conductor.tracks = [] |
30
+ .current_agent = null |
31
+ .next_agent = "none" |
32
+ .agent_status = "operating" |
33
+ .owner_prompt.status = "operating" |
34
+ .conductor.stop_chain_count = 0 |
35
+ .operating.cycles = ((.operating.cycles // 0) + 1) |
36
+ .operating.last_cycle_at = (now | todate) |
37
+ .operating.goal = $goal' \
38
+ --arg reason "$REASON" --arg goal "$GOAL_REL"; then
39
+ echo "[company-cycle] FAILED: progress.json operating heartbeat did not apply (goal=$GOAL_REL). Runtime is NOT marked operating." >&2
40
+ exit 1
41
+ fi
42
+
43
+ # bump the per-goal agenda cycle counter too (best-effort)
44
+ AGENDA="$PROJECT_ROOT/.harness/documents/$GOAL_REL/agenda.json"
45
+ if [ -n "$GOAL_REL" ] && [ -f "$AGENDA" ]; then
46
+ tmp="$AGENDA.tmp.$$"
47
+ jq '.cycles = ((.cycles // 0) + 1) | .last_cycle_at = (now | todate)' "$AGENDA" > "$tmp" && mv "$tmp" "$AGENDA" || rm -f "$tmp"
48
+ fi
49
+
50
+ if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then
51
+ node "$SCRIPT_DIR/harness-activity-record.js" "$PROJECT_ROOT" >/dev/null 2>&1 || true
52
+ fi
53
+
54
+ echo "[company-cycle] operating heartbeat recorded: $REASON"
@@ -17,10 +17,16 @@ PROJECT_ROOT="${1:-.}"
17
17
  FILTER="${2:-}"
18
18
 
19
19
  if [ -z "$FILTER" ]; then
20
- echo "[progress-set] usage: $0 <project-root> <jq_filter>" >&2
20
+ echo "[progress-set] usage: $0 <project-root> <jq_filter> [jq-args...]" >&2
21
21
  exit 2
22
22
  fi
23
23
 
24
+ # Any args after the filter are passed straight through to jq (e.g.
25
+ # `--arg reason "$REASON"`). Callers MUST inject untrusted/free-text values this
26
+ # way and reference them as jq $variables — never string-interpolate into the
27
+ # filter, which breaks on quotes and can silently fail.
28
+ shift 2 2>/dev/null || true
29
+
24
30
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
31
 
26
32
  if [ ! -f "$PROGRESS" ]; then
@@ -32,7 +38,7 @@ fi
32
38
  FINAL_FILTER="$FILTER | .updated_at = (now | todate)"
33
39
 
34
40
  TMP="${PROGRESS}.tmp.$$"
35
- if jq "$FINAL_FILTER" "$PROGRESS" > "$TMP" 2>/dev/null; then
41
+ if jq "$@" "$FINAL_FILTER" "$PROGRESS" > "$TMP" 2>/dev/null; then
36
42
  mv "$TMP" "$PROGRESS"
37
43
  else
38
44
  rm -f "$TMP"
@@ -51,6 +51,76 @@ if [ "$HOOK_EVENT" = "Stop" ]; then
51
51
  if [ "$STOP_HOOK_ACTIVE" = "true" ]; then exit 0; fi
52
52
  fi
53
53
 
54
+ # ── v7 깨끗한 정지 short-circuit ──────────────────────────────
55
+ # 회사 루프의 정당한 정지 조건은 둘뿐이다: COMPLETE, 그리고 외부권한 BLOCKED.
56
+ # harness-company-complete.sh / harness-company-block.sh 가 이 terminal 런타임
57
+ # 상태를 기록한다. 그 상태를 여기서 존중해, 끝났는데도 "계속하라"고 채근하지 않고
58
+ # 턴을 깨끗이 종료한다. (/goal 직후 routing/running 상태에서는 발동하지 않는다.)
59
+ case "$CONDUCTOR_STATE" in
60
+ completed|idle|blocked) exit 0 ;;
61
+ esac
62
+ if [ "$AGENT_STATUS" = "blocked" ] || [ "$OWNER_PROMPT_STATUS" = "completed" ] || [ "$OWNER_PROMPT_STATUS" = "awaiting-authority" ]; then
63
+ exit 0
64
+ fi
65
+
66
+ # ── v7 영구 운영(operating) + 완료 backstop ──────────────────
67
+ # .harness/documents 의 mission-state 를 스캔해 루프 종류를 분류한다:
68
+ # - operating mission 이 active → 영구 모드: 안건을 강제 구동하거나 wake 로 양보
69
+ # - 모든 mission 이 terminal → 유한 루프 종료: 완료 처리 + 깨끗한 정지
70
+ # (mission 문서가 없으면 순수 v6 프로젝트 → 미발동)
71
+ DOCS_DIR="$CWD/.harness/documents"
72
+ if [ "$TASK_STOP_ACTIVE" != "true" ] && [ -d "$DOCS_DIR" ] && \
73
+ { [ "$CONDUCTOR_STATE" = "running" ] || [ "$CONDUCTOR_STATE" = "operating" ]; }; then
74
+ any_mission="false"; active_mission="false"; operating_active="false"; operating_rel=""
75
+ while IFS= read -r state; do
76
+ [ -n "$state" ] || continue
77
+ any_mission="true"
78
+ a=$(jq -r '.active // false' "$state" 2>/dev/null || echo false)
79
+ lc=$(jq -r '.lifecycle // .status // "unknown"' "$state" 2>/dev/null || echo unknown)
80
+ case "$lc" in
81
+ closed|cancelled|superseded|complete|completed|blocked) ;;
82
+ operating|monitoring)
83
+ if [ "$a" = "true" ]; then
84
+ active_mission="true"; operating_active="true"
85
+ rel="${state#"$DOCS_DIR"/}"; operating_rel="${rel%/mission-state.json}"
86
+ fi ;;
87
+ *) [ "$a" = "true" ] && active_mission="true" ;;
88
+ esac
89
+ done < <(find "$DOCS_DIR" -name mission-state.json -type f 2>/dev/null)
90
+
91
+ if [ "$operating_active" = "true" ]; then
92
+ active_agenda=0
93
+ if [ -x "$SCRIPT_DIR/harness-agenda.sh" ]; then
94
+ active_agenda=$(bash "$SCRIPT_DIR/harness-agenda.sh" "$CWD" "$operating_rel" active-count 2>/dev/null || echo 0)
95
+ fi
96
+ # operating tick 도 활동 샘플 1회 + stop_chain_count 증가
97
+ NEW_COUNT=$((STOP_CHAIN_COUNT + 1))
98
+ jq --argjson n "$NEW_COUNT" '.conductor.stop_chain_count=$n | .conductor.last_stop_chain_at=(now|todate)' "$PROGRESS" > "$PROGRESS.tmp.$$" 2>/dev/null && mv "$PROGRESS.tmp.$$" "$PROGRESS" || rm -f "$PROGRESS.tmp.$$"
99
+ if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then node "$SCRIPT_DIR/harness-activity-record.js" "$CWD" >/dev/null 2>&1 || true; fi
100
+ if [ "${active_agenda:-0}" -gt 0 ] 2>/dev/null; then
101
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$CWD" '.conductor.state="running"' >/dev/null 2>&1 || true
102
+ jq -nc --arg n "$active_agenda" --arg g "$operating_rel" '{decision:"block", reason:("영구 운영 루프: 활성 안건 \($n)건. CEO는 .harness/documents/\($g)/agenda.json 의 모든 open 안건을 `scripts/harness-agenda.sh . \($g) decide` 로 판단·라우팅하고, decided 안건은 담당 CXX가 hired worker로 실행·검증한 뒤 `... close` 할 때까지 진행하라. 안건이 남았는데 턴을 끝내지 말 것.")}'
103
+ exit 0
104
+ elif [ "$CONDUCTOR_STATE" = "operating" ]; then
105
+ exit 0 # 안건 0 + heartbeat 기록됨 → 다음 hourly wake 로 양보(깨끗한 정지)
106
+ else
107
+ jq -nc --arg g "$operating_rel" '{decision:"block", reason:("영구 운영 루프: 안건 없음. CEO는 각 CXX(COO/CDO/CTO/CQO/OPS)에게 현황 보고를 지시하라 — 각 CXX는 자기 산하 worker 산출물이 goal 기준으로 여전히 올바르게 동작하는지 브리핑하고, 손실·드리프트·기회·리스크가 보이면 `scripts/harness-agenda.sh . \($g) raise <cxx> <kind> \"<title>\"` 로 새 안건을 등록한다. 한 바퀴 브리핑이 정말 무안건이면 `scripts/harness-company-cycle.sh . \($g)` 로 operating heartbeat 를 남기고 다음 hourly wake 로 양보하라.")}'
108
+ exit 0
109
+ fi
110
+ fi
111
+
112
+ if [ "$any_mission" = "true" ] && [ "$active_mission" = "false" ] && [ "$CONDUCTOR_STATE" = "running" ]; then
113
+ # Never silently complete: record why the backstop fired so an operator can
114
+ # audit a misclassification, and only treat the loop as finished if the
115
+ # transition actually applied (otherwise fall through and keep chaining).
116
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | stop-hook | auto-complete | backstop fired (conductor=running but no active mission under .harness/documents)" >> "$CWD/.harness/progress.log" 2>/dev/null || true
117
+ if bash "$SCRIPT_DIR/harness-company-complete.sh" "$CWD" "loop-idle-no-active-mission" >/dev/null 2>&1; then
118
+ exit 0
119
+ fi
120
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | stop-hook | auto-complete | WARN: harness-company-complete.sh failed during backstop; continuing loop" >> "$CWD/.harness/progress.log" 2>/dev/null || true
121
+ fi
122
+ fi
123
+
54
124
  # 자동 연쇄 조건:
55
125
  # conductor/company loop 가 running
56
126
  # AND task_stop 비활성
@@ -104,6 +174,12 @@ jq --argjson n "$NEW_COUNT" '.conductor.stop_chain_count = $n |
104
174
  .conductor.last_stop_chain_at = (now | todate)' "$PROGRESS" > "$TMP" 2>/dev/null \
105
175
  && mv "$TMP" "$PROGRESS" || rm -f "$TMP"
106
176
 
177
+ # 자율 tick 마다 활동 샘플 1회 기록 → 대시보드 히트맵이 라이브 운영 중에도 채워진다.
178
+ # (recorder 의 runtimeIdle 게이트가 완료 후에는 count>0 샘플을 알아서 억제한다.)
179
+ if command -v node >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/harness-activity-record.js" ]; then
180
+ node "$SCRIPT_DIR/harness-activity-record.js" "$CWD" >/dev/null 2>&1 || true
181
+ fi
182
+
107
183
  # Claude 에게 다음 행동 지시
108
184
  if [ "$ESCALATION" != "null" ]; then
109
185
  REASON="자율 회사 루프 진행 중. escalation=${ESCALATION} 는 정지 사유가 아니라 meeting-manager/service-ops 공유 대상이다. current_agent=${CURRENT_AGENT}, next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}, company=${COMPANY_STATE}. SKILL 에 따라 CEO/CXX/worker 흐름을 계속하거나 conductor-tick 을 굴려 meeting-manager 회의로 라우팅하라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
@@ -0,0 +1,151 @@
1
+ #!/bin/bash
2
+ # harness-wake-control.sh — per-project hourly-wake launchd control, keyed by HARNESS_BASE_PORT.
3
+ #
4
+ # The perpetual operating loop (Mode 1) relies on the hourly launchd wake to
5
+ # resume cycles across sessions. This gives the dashboard a deterministic,
6
+ # per-project on/off handle: the launchd job Label is anchored to the project's
7
+ # HARNESS_BASE_PORT (e.g. com.walwal.harness-wake.43000), so each project's wake
8
+ # daemon is uniquely addressable and independently toggleable.
9
+ #
10
+ # launchd periodic jobs are StartInterval execs, not listening daemons, so there
11
+ # is no TCP port to bind — the base-port-keyed Label IS the fixed control id.
12
+ #
13
+ # Usage (all print JSON status except `label`):
14
+ # harness-wake-control.sh status <project-root>
15
+ # harness-wake-control.sh on <project-root> [--dry-run]
16
+ # harness-wake-control.sh off <project-root>
17
+ # harness-wake-control.sh label <project-root>
18
+
19
+ set -uo pipefail
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
+ WAKE_SH="$SCRIPT_DIR/harness-wake.sh"
23
+ INSTALL_SH="$SCRIPT_DIR/harness-wake-install.sh"
24
+ CMD="${1:-status}"
25
+ PROJECT_ROOT="${2:-$PWD}"
26
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" 2>/dev/null && pwd || echo "$PROJECT_ROOT")"
27
+ DRYRUN=0
28
+ [ "${3:-}" = "--dry-run" ] && DRYRUN=1
29
+
30
+ LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
31
+ LOG_DIR="$HOME/.walwal-harness/logs"
32
+ PROJECTS_LIST="$HOME/.walwal-harness/projects.list"
33
+ STATE_DIR="$PROJECT_ROOT/.harness/runtime"
34
+ GLOBAL_LABEL="com.walwal.harness-wake"
35
+ DOMAIN="gui/$(id -u 2>/dev/null || echo 0)"
36
+
37
+ command -v jq >/dev/null 2>&1 || { echo '{"supported":false,"error":"jq required"}'; exit 0; }
38
+
39
+ resolve_base_port() {
40
+ local p=""
41
+ if [ -f "$PROJECT_ROOT/.env" ]; then
42
+ p=$(grep -E '^[[:space:]]*HARNESS_BASE_PORT=' "$PROJECT_ROOT/.env" 2>/dev/null | tail -1 | cut -d= -f2 | tr -dc '0-9')
43
+ fi
44
+ if [ -z "$p" ] && [ -f "$PROJECT_ROOT/.harness/config.json" ]; then
45
+ p=$(jq -r '.runtime.ports.base // empty' "$PROJECT_ROOT/.harness/config.json" 2>/dev/null | tr -dc '0-9')
46
+ fi
47
+ echo "$p"
48
+ }
49
+
50
+ BASE_PORT="$(resolve_base_port)"
51
+ if [ -n "$BASE_PORT" ]; then KEY="$BASE_PORT"; else KEY="p$(printf '%s' "$PROJECT_ROOT" | cksum | cut -d' ' -f1)"; fi
52
+ LABEL="$GLOBAL_LABEL.$KEY"
53
+ PLIST="$LAUNCH_AGENTS_DIR/$LABEL.plist"
54
+
55
+ supported() { [ "$(uname 2>/dev/null)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; }
56
+ is_loaded() { launchctl print "$DOMAIN/$1" >/dev/null 2>&1; }
57
+ in_global_list() { [ -f "$PROJECTS_LIST" ] && grep -Fxq "$PROJECT_ROOT" "$PROJECTS_LIST" 2>/dev/null; }
58
+
59
+ # XML-escape values interpolated into the plist (paths may contain & < > " ').
60
+ xml_escape() {
61
+ printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
62
+ }
63
+
64
+ write_plist() { # path
65
+ mkdir -p "$(dirname "$1")"
66
+ local e_label e_wake e_out e_err e_path e_root
67
+ e_label="$(xml_escape "$LABEL")"
68
+ e_wake="$(xml_escape "$WAKE_SH")"
69
+ e_out="$(xml_escape "$LOG_DIR/wake.$KEY.stdout.log")"
70
+ e_err="$(xml_escape "$LOG_DIR/wake.$KEY.stderr.log")"
71
+ e_path="$(xml_escape "$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin")"
72
+ e_root="$(xml_escape "$PROJECT_ROOT")"
73
+ cat > "$1" <<PL
74
+ <?xml version="1.0" encoding="UTF-8"?>
75
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
76
+ <plist version="1.0">
77
+ <dict>
78
+ <key>Label</key><string>$e_label</string>
79
+ <key>ProgramArguments</key>
80
+ <array><string>/bin/bash</string><string>$e_wake</string></array>
81
+ <key>StartInterval</key><integer>3600</integer>
82
+ <key>RunAtLoad</key><true/>
83
+ <key>StandardOutPath</key><string>$e_out</string>
84
+ <key>StandardErrorPath</key><string>$e_err</string>
85
+ <key>EnvironmentVariables</key>
86
+ <dict>
87
+ <key>PATH</key><string>$e_path</string>
88
+ <key>WALWAL_HARNESS_PROJECTS</key><string>$e_root</string>
89
+ </dict>
90
+ </dict>
91
+ </plist>
92
+ PL
93
+ }
94
+
95
+ write_state() { # enabled-bool
96
+ mkdir -p "$STATE_DIR"
97
+ jq -n --arg label "$LABEL" --arg plist "$PLIST" --arg base "$BASE_PORT" --arg key "$KEY" \
98
+ --arg root "$PROJECT_ROOT" --argjson enabled "$1" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
99
+ {label:$label, plist:$plist, key:$key,
100
+ base_port:($base | if . == "" then null else (tonumber? // .) end),
101
+ project_root:$root, enabled:$enabled, updated_at:$now}' \
102
+ > "$STATE_DIR/wake-control.json" 2>/dev/null || true
103
+ }
104
+
105
+ status_json() {
106
+ local sup="false" pp="false" gl="false" ing="false" eff="false" plx="false"
107
+ supported && sup="true"
108
+ if supported; then
109
+ is_loaded "$LABEL" && pp="true"
110
+ is_loaded "$GLOBAL_LABEL" && gl="true"
111
+ fi
112
+ in_global_list && ing="true"
113
+ [ -f "$PLIST" ] && plx="true"
114
+ if [ "$pp" = "true" ] || { [ "$gl" = "true" ] && [ "$ing" = "true" ]; }; then eff="true"; fi
115
+ jq -nc \
116
+ --argjson supported "$sup" --argjson enabled "$eff" \
117
+ --argjson per_project_loaded "$pp" --argjson global_loaded "$gl" --argjson in_global_list "$ing" \
118
+ --argjson plist_exists "$plx" \
119
+ --arg label "$LABEL" --arg key "$KEY" --arg plist "$PLIST" --arg root "$PROJECT_ROOT" \
120
+ --arg base "$BASE_PORT" '
121
+ {supported:$supported, enabled:$enabled,
122
+ per_project_loaded:$per_project_loaded, global_loaded:$global_loaded, in_global_list:$in_global_list,
123
+ plist_exists:$plist_exists, label:$label, key:$key, plist:$plist, project_root:$root,
124
+ base_port:($base | if . == "" then null else (tonumber? // .) end),
125
+ interval_seconds:3600}'
126
+ }
127
+
128
+ case "$CMD" in
129
+ label) echo "$LABEL" ;;
130
+ status) status_json ;;
131
+ on)
132
+ if ! supported; then status_json; exit 0; fi
133
+ if [ "$DRYRUN" = "1" ]; then write_plist "$STATE_DIR/$LABEL.plist.preview"; write_state true; status_json; exit 0; fi
134
+ write_plist "$PLIST"
135
+ launchctl bootout "$DOMAIN/$LABEL" >/dev/null 2>&1 || launchctl unload "$PLIST" >/dev/null 2>&1 || true
136
+ launchctl bootstrap "$DOMAIN" "$PLIST" >/dev/null 2>&1 || launchctl load "$PLIST" >/dev/null 2>&1 || true
137
+ # per-project job is authoritative; drop from the legacy global list to avoid double-wake.
138
+ [ -x "$INSTALL_SH" ] && bash "$INSTALL_SH" remove "$PROJECT_ROOT" >/dev/null 2>&1 || true
139
+ write_state true
140
+ status_json
141
+ ;;
142
+ off)
143
+ if ! supported; then status_json; exit 0; fi
144
+ launchctl bootout "$DOMAIN/$LABEL" >/dev/null 2>&1 || launchctl unload "$PLIST" >/dev/null 2>&1 || true
145
+ rm -f "$PLIST"
146
+ [ -x "$INSTALL_SH" ] && bash "$INSTALL_SH" remove "$PROJECT_ROOT" >/dev/null 2>&1 || true
147
+ write_state false
148
+ status_json
149
+ ;;
150
+ *) echo '{"error":"usage: status|on|off|label <project-root> [--dry-run]"}' >&2; exit 2 ;;
151
+ esac
@@ -167,6 +167,11 @@ Project root: $project_root
167
167
  Wake reason: idle=${idle}s force=${force}
168
168
  Hourly review: ${review_path:-"(not available)"}
169
169
 
170
+ Operating (perpetual) goal mode:
171
+ - If the active goal's mission-state.json lifecycle is "operating", this is a never-completing standing company. Do NOT run harness-company-complete.sh for it.
172
+ - Read the shared agenda .harness/documents/<goal>/agenda.json (scripts/harness-agenda.sh . <goal> list). If it has active items, adjudicate every open item (scripts/harness-agenda.sh . <goal> decide <id> "<decision>" <cxx>) and route the owning CXX to execute+verify, then close it. If the agenda is empty, run a status-briefing round: require each CXX (COO/CDO/CTO/CQO/OPS) to confirm its live deliverables still operate toward the goal and to raise any loss/drift/incident/opportunity/risk via scripts/harness-agenda.sh . <goal> raise <cxx> <kind> "<title>".
173
+ - If a full briefing round genuinely surfaces nothing this tick, run scripts/harness-company-cycle.sh . <goal> to record the operating heartbeat and end the tick; the next hourly wake resumes the cycle. A loss/drawdown is an agenda item that triggers the next research→apply→operate cycle, never a reason to stop or to wait on the Owner.
174
+
170
175
  One-line command:
171
176
  Convene CXX + OPS, collect current progress and decisions, decide the next action as CEO with the active goal in mind, drive all teams without asking Owner, and use CQO verification until the goal demonstrably works.
172
177