create-harness-vibe-coding 0.8.17 → 0.8.19

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 (150) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +3 -0
  4. package/package.json +5 -2
  5. package/src/generator.js +613 -97
  6. package/src/index.js +556 -309
  7. package/src/prompts.js +18 -0
  8. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  9. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  10. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  11. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  12. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  13. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  14. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  15. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  16. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  17. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  18. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  19. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  20. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  21. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  22. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  23. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  24. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  25. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  26. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  27. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  28. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  29. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  30. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  31. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  32. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  33. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  34. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  35. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  36. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  37. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  38. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  39. package/src/ui/dist/index.html +18 -0
  40. package/src/ui/index.html +17 -0
  41. package/src/ui/package.json +33 -0
  42. package/src/ui/pnpm-lock.yaml +1582 -0
  43. package/src/ui/pnpm-workspace.yaml +2 -0
  44. package/src/ui/src/App.tsx +86 -0
  45. package/src/ui/src/api.ts +93 -0
  46. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  47. package/src/ui/src/components/Footer.tsx +69 -0
  48. package/src/ui/src/components/Header.tsx +175 -0
  49. package/src/ui/src/components/LoadingView.tsx +22 -0
  50. package/src/ui/src/components/RolesRoute.tsx +169 -0
  51. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  52. package/src/ui/src/components/TaskList.tsx +388 -0
  53. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  54. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  55. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  56. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  57. package/src/ui/src/index.css +281 -0
  58. package/src/ui/src/main.tsx +11 -0
  59. package/src/ui/src/types.ts +108 -0
  60. package/src/ui/tsconfig.json +21 -0
  61. package/src/ui/vite.config.ts +19 -0
  62. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  63. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  64. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  65. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  66. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  67. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  68. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  69. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  70. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  71. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  72. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  73. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  74. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  75. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  76. package/src/wf-ui-server/a2a-store.mjs +296 -0
  77. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  78. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  79. package/src/wf-ui-server/runtime-config.mjs +153 -0
  80. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  81. package/src/wf-ui-server/security.mjs +67 -0
  82. package/src/wf-ui-server/server.mjs +849 -0
  83. package/src/wf-ui-server/session-registry.mjs +204 -0
  84. package/src/wf-ui-server/settings.mjs +100 -0
  85. package/src/wf-ui-server/task-parser.mjs +133 -0
  86. package/src/wf-ui-server/terminal-store.mjs +225 -0
  87. package/src/wf-ui-server/token.mjs +41 -0
  88. package/src/wf-ui-server/ws-events.mjs +354 -0
  89. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  90. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  91. package/templates/common/.claude/commands/wf-help.md +5 -0
  92. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  93. package/templates/common/.claude/commands/wf-task-list.md +26 -0
  94. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  95. package/templates/common/.claude/commands/wf-ui.md +26 -0
  96. package/templates/common/.claude/commands/wf-update.md +54 -9
  97. package/templates/common/.claude/rules/ecc/common.md +1 -1
  98. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  99. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  100. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  101. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  102. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  103. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  104. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  105. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  106. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  107. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  108. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  109. package/templates/common/.harness-version +105 -47
  110. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  111. package/templates/common/.opencode/commands/wf-help.md +5 -0
  112. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  113. package/templates/common/.opencode/commands/wf-task-list.md +29 -0
  114. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  115. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  116. package/templates/common/.opencode/commands/wf-update.md +54 -9
  117. package/templates/common/CLAUDE.md +8 -6
  118. package/templates/common/Harness/MEMORY.md +11 -0
  119. package/templates/common/Harness/README.md +21 -38
  120. package/templates/common/Harness/a2a/role-graph.json +79 -0
  121. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  122. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  123. package/templates/common/Harness/ownership.manifest.json +142 -2
  124. package/templates/common/Harness/scripts/README.md +134 -0
  125. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  126. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  127. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  128. package/templates/common/Harness/scripts/task-state.mjs +949 -26
  129. package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
  130. package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
  131. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  132. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  133. package/templates/common/Harness/settings.json +35 -0
  134. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  135. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  136. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
  137. package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
  138. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  139. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  140. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  141. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  142. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  143. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  144. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  145. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  146. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  147. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  148. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
  149. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  150. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
@@ -0,0 +1,134 @@
1
+ # Harness Scripts
2
+
3
+ Agents: use `task-state.mjs` for all task lifecycle operations. Other scripts are purpose-built tools invoked on demand.
4
+
5
+ ## Task Lifecycle (primary)
6
+
7
+ ### `task-state.mjs`
8
+
9
+ Single entry point for all task capsule operations. Use this for any task CRUD.
10
+
11
+ ```bash
12
+ # Active tasks
13
+ node Harness/scripts/task-state.mjs list [--json]
14
+ node Harness/scripts/task-state.mjs open [--json]
15
+ node Harness/scripts/task-state.mjs validate [--strict] [--json]
16
+ node Harness/scripts/task-state.mjs reconcile [--dry-run|--apply] [--json]
17
+ node Harness/scripts/task-state.mjs set-active <task-id>
18
+ node Harness/scripts/task-state.mjs transition <task-id> --status <s> --phase <p>
19
+
20
+ # Task creation
21
+ node Harness/scripts/task-state.mjs record <task-id> --create --text "goal" [--status <s>] [--dry-run|--apply]
22
+ node Harness/scripts/task-state.mjs record --title "title" --note "note" # auto-generates ID
23
+
24
+ # Archive (moves completed tasks to _archive/YYYY/MM/DD/)
25
+ node Harness/scripts/task-state.mjs archive [--dry-run|--apply] [--keep n] [--task id] [--json]
26
+
27
+ # History (archived task management)
28
+ node Harness/scripts/task-state.mjs history list [--year YYYY] [--month MM] [--json]
29
+ node Harness/scripts/task-state.mjs history search <keyword> [--json]
30
+ node Harness/scripts/task-state.mjs history load <task-id> [--json]
31
+ node Harness/scripts/task-state.mjs history delete <task-id> [--dry-run|--apply] [--json]
32
+ ```
33
+
34
+ **When to use:**
35
+ - `/wf-task-list` calls `task-state.mjs list`
36
+ - `/wf-task-archive` calls `task-state.mjs archive`
37
+ - `/wf-task-record` calls `task-state.mjs record`
38
+ - Resume: check `task-state.mjs open` for blocked/open tasks
39
+ - Closeout: run `task-state.mjs archive --apply` to clean up verified tasks
40
+ - Debug: run `task-state.mjs validate --strict` to check state integrity
41
+ - Research: run `task-state.mjs history search <keyword>` to find past solutions
42
+
43
+ ### `archive-tasks.mjs`
44
+
45
+ Compatibility alias for `task-state.mjs archive`. Delegates all flags.
46
+
47
+ ## Validation
48
+
49
+ ### `validate-harness.mjs`
50
+
51
+ Validates harness structure, task state, file integrity.
52
+
53
+ ```bash
54
+ node Harness/scripts/validate-harness.mjs [--strict] [--json]
55
+ ```
56
+
57
+ **When to use:** pre-commit checks, CI gates, release validation. Strict mode fails on any violation.
58
+
59
+ ## Maintenance
60
+
61
+ ### `wf-remove.mjs`
62
+
63
+ Safely removes the Harness scaffold from a project.
64
+
65
+ ```bash
66
+ node Harness/scripts/wf-remove.mjs [--dry-run|--apply] [--json]
67
+ ```
68
+
69
+ **When to use:** uninstalling Harness, cleaning up test installs. Always dry-run first.
70
+
71
+ ### `wf-update-check.mjs`
72
+
73
+ Checks if a newer Harness version is available.
74
+
75
+ ```bash
76
+ node Harness/scripts/wf-update-check.mjs [--json]
77
+ ```
78
+
79
+ **When to use:** `/wf-update` command, periodic drift detection.
80
+
81
+ ### `wf-auto-update-prompt.mjs`
82
+
83
+ Generates context-aware update prompts for WF-AUTO cycles.
84
+
85
+ ```bash
86
+ node Harness/scripts/wf-auto-update-prompt.mjs
87
+ ```
88
+
89
+ **When to use:** internally by WF-AUTO/WF-AUTO-SPARK cycles. Not invoked directly.
90
+
91
+ ### `scan-clean.mjs`
92
+
93
+ Scans and cleans stale artifacts, temp files, orphaned references.
94
+
95
+ ```bash
96
+ node Harness/scripts/scan-clean.mjs [--dry-run|--apply] [--json]
97
+ ```
98
+
99
+ **When to use:** before releases, after large refactors, periodic housekeeping.
100
+
101
+ ## Telemetry
102
+
103
+ ### `l2-cache-telemetry.mjs`
104
+
105
+ Measures prompt-cache hit rates across CLI runtimes (Claude Code, Codex, OpenCode).
106
+
107
+ ```bash
108
+ node Harness/scripts/l2-cache-telemetry.mjs --groups <g1>,<g2> --turns <n> [--json]
109
+ ```
110
+
111
+ **When to use:** benchmarking cache performance, tuning context loading, regression detection.
112
+
113
+ ### `context-budget.mjs`
114
+
115
+ Estimates context budget usage for a given task or workflow tier.
116
+
117
+ ```bash
118
+ node Harness/scripts/context-budget.mjs [--tier wf-light|wf-standard|wf-full|wf-max]
119
+ ```
120
+
121
+ **When to use:** before large workflows, capacity planning, timeout risk assessment.
122
+
123
+ ## Agent Quick Reference
124
+
125
+ | Goal | Command |
126
+ |------|---------|
127
+ | List active tasks + graph | `node Harness/scripts/task-state.mjs list` |
128
+ | Find blocked tasks | `node Harness/scripts/task-state.mjs open --json` |
129
+ | Create new task | `node Harness/scripts/task-state.mjs record --title "..." --note "..." --apply` |
130
+ | Archive completed tasks | `node Harness/scripts/task-state.mjs archive --apply` |
131
+ | Search history for "timeout" | `node Harness/scripts/task-state.mjs history search timeout --json` |
132
+ | Load old task full record | `node Harness/scripts/task-state.mjs history load <task-id> --json` |
133
+ | Validate state integrity | `node Harness/scripts/validate-harness.mjs --strict` |
134
+ | Pre-release cleanup | `node Harness/scripts/scan-clean.mjs --dry-run` |
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+
6
+ const MAX_LIMIT = 2000;
7
+
8
+ function parseArgs(argv) {
9
+ const [command, ...rest] = argv;
10
+ const flags = {};
11
+ for (let i = 0; i < rest.length; i += 1) {
12
+ const arg = rest[i];
13
+ if (!arg.startsWith('--')) continue;
14
+ const key = arg.slice(2);
15
+ const value = rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[++i] : 'true';
16
+ flags[key] = value;
17
+ }
18
+ return { command, flags };
19
+ }
20
+
21
+ function validId(id) {
22
+ return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
23
+ }
24
+
25
+ function readJson(filePath, fallback = null) {
26
+ try {
27
+ if (!fs.existsSync(filePath)) return fallback;
28
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
29
+ } catch {
30
+ return fallback;
31
+ }
32
+ }
33
+
34
+ function readJsonl(filePath) {
35
+ try {
36
+ if (!fs.existsSync(filePath)) return [];
37
+ return fs.readFileSync(filePath, 'utf8')
38
+ .split(/\r?\n/)
39
+ .filter(Boolean)
40
+ .map((line) => JSON.parse(line));
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function appendJsonl(filePath, row) {
47
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
48
+ fs.appendFileSync(filePath, `${JSON.stringify(row)}\n`, 'utf8');
49
+ }
50
+
51
+ function allSessionDirs(projectRoot) {
52
+ const tasksRoot = path.join(projectRoot, 'Harness', 'tasks');
53
+ let tasks = [];
54
+ try { tasks = fs.readdirSync(tasksRoot, { withFileTypes: true }); } catch { return []; }
55
+ const out = [];
56
+ for (const task of tasks) {
57
+ if (!task.isDirectory() || task.name.startsWith('_')) continue;
58
+ const sessionsRoot = path.join(tasksRoot, task.name, 'sessions');
59
+ let sessions = [];
60
+ try { sessions = fs.readdirSync(sessionsRoot, { withFileTypes: true }); } catch { continue; }
61
+ for (const session of sessions) {
62
+ if (session.isDirectory()) out.push({ taskId: task.name, sessionId: session.name, dir: path.join(sessionsRoot, session.name) });
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ function findSession(projectRoot, sessionId) {
69
+ if (!validId(sessionId)) return null;
70
+ return allSessionDirs(projectRoot).find((session) => session.sessionId === sessionId) || null;
71
+ }
72
+
73
+ function globToRegex(pattern) {
74
+ const escaped = String(pattern || '**/*')
75
+ .replace(/\\/g, '/')
76
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
77
+ .replace(/\*\*/g, '::DOUBLE_STAR::')
78
+ .replace(/\*/g, '[^/]*')
79
+ .replace(/::DOUBLE_STAR::/g, '.*');
80
+ return new RegExp(`^${escaped}$`);
81
+ }
82
+
83
+ function print(data) {
84
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
85
+ }
86
+
87
+ function listSessions(projectRoot) {
88
+ return allSessionDirs(projectRoot)
89
+ .map(({ dir }) => readJson(path.join(dir, 'STATE.json')))
90
+ .filter(Boolean)
91
+ .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
92
+ }
93
+
94
+ function readRange(projectRoot, flags) {
95
+ const found = findSession(projectRoot, flags.session);
96
+ if (!found) return { sessionId: flags.session, entries: [] };
97
+ let entries = readJsonl(path.join(found.dir, 'terminal.jsonl'));
98
+ const from = Number(flags.from);
99
+ const to = Number(flags.to);
100
+ const tail = Number(flags.tail);
101
+ if (Number.isFinite(from)) entries = entries.filter((entry) => entry.seq >= from);
102
+ if (Number.isFinite(to)) entries = entries.filter((entry) => entry.seq <= to);
103
+ if (Number.isFinite(tail) && tail > 0) entries = entries.slice(-Math.min(tail, MAX_LIMIT));
104
+ else entries = entries.slice(0, MAX_LIMIT);
105
+ return { taskId: found.taskId, sessionId: found.sessionId, entries };
106
+ }
107
+
108
+ function globEvents(projectRoot, flags) {
109
+ const regex = globToRegex(flags.pattern || flags.glob || '**/*');
110
+ const limit = Math.min(Math.max(Number(flags.limit) || 200, 1), MAX_LIMIT);
111
+ const sinceMs = flags.since ? new Date(flags.since).getTime() : null;
112
+ const rows = [];
113
+ for (const session of allSessionDirs(projectRoot)) {
114
+ for (const filename of ['events.jsonl', 'terminal.jsonl', 'input-requests.jsonl']) {
115
+ const rel = `Harness/tasks/${session.taskId}/sessions/${session.sessionId}/${filename}`;
116
+ if (!regex.test(rel)) continue;
117
+ for (const row of readJsonl(path.join(session.dir, filename))) {
118
+ const tsMs = row.ts ? new Date(row.ts).getTime() : 0;
119
+ if (sinceMs && tsMs < sinceMs) continue;
120
+ rows.push({ source: rel, ...row });
121
+ }
122
+ }
123
+ }
124
+ return rows.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || ''))).slice(-limit);
125
+ }
126
+
127
+ function postInput(urlText, token, sessionId, text) {
128
+ return new Promise((resolve, reject) => {
129
+ const base = new URL(urlText);
130
+ const body = JSON.stringify({ data: text });
131
+ const req = http.request({
132
+ hostname: base.hostname,
133
+ port: base.port,
134
+ path: `/api/sessions/${encodeURIComponent(sessionId)}/input`,
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ 'Content-Length': Buffer.byteLength(body),
139
+ Authorization: `Bearer ${token}`,
140
+ },
141
+ }, (res) => {
142
+ let data = '';
143
+ res.on('data', chunk => { data += chunk; });
144
+ res.on('end', () => {
145
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
146
+ catch { resolve({ status: res.statusCode, body: data }); }
147
+ });
148
+ });
149
+ req.on('error', reject);
150
+ req.write(body);
151
+ req.end();
152
+ });
153
+ }
154
+
155
+ async function sendInput(projectRoot, flags) {
156
+ const sessionId = flags.session;
157
+ const text = flags.text || '';
158
+ const url = flags.url || process.env.WF_UI_URL;
159
+ const token = flags.token || process.env.WF_UI_TOKEN;
160
+ if (url && token) return postInput(url, token, sessionId, text);
161
+
162
+ const found = findSession(projectRoot, sessionId);
163
+ if (!found) throw new Error(`Session not found: ${sessionId}`);
164
+ const state = readJson(path.join(found.dir, 'STATE.json'));
165
+ if (!state?.attachMode) throw new Error('Input rejected: session is in watch mode. Enable attach mode first.');
166
+ const row = {
167
+ ts: new Date().toISOString(),
168
+ type: 'terminal.input.requested',
169
+ taskId: found.taskId,
170
+ sessionId,
171
+ data: text,
172
+ };
173
+ appendJsonl(path.join(found.dir, 'input-requests.jsonl'), row);
174
+ return { queued: true, ...row };
175
+ }
176
+
177
+ async function main() {
178
+ const { command, flags } = parseArgs(process.argv.slice(2));
179
+ const projectRoot = path.resolve(flags.project || process.cwd());
180
+ if (command === 'list-sessions') return print(listSessions(projectRoot));
181
+ if (command === 'read-range') return print(readRange(projectRoot, flags));
182
+ if (command === 'tail') return print(readRange(projectRoot, { ...flags, tail: flags.lines || flags.tail || 100 }));
183
+ if (command === 'glob-events') return print(globEvents(projectRoot, flags));
184
+ if (command === 'send-input') return print(await sendInput(projectRoot, flags));
185
+ throw new Error('Usage: a2a-terminal.mjs list-sessions|read-range|tail|glob-events|send-input [--project .]');
186
+ }
187
+
188
+ main().catch((err) => {
189
+ process.stderr.write(`${err.message}\n`);
190
+ process.exit(1);
191
+ });
@@ -20,7 +20,7 @@ const routes = [
20
20
  },
21
21
  {
22
22
  id: 'wf-router-prefix',
23
- maxBytes: 42000,
23
+ maxBytes: 43000,
24
24
  paths: [
25
25
  'CLAUDE.md',
26
26
  'Harness/MEMORY.md',
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const args = process.argv.slice(2);
9
+
10
+ function readFlagValue(flagName) {
11
+ const idx = args.indexOf(flagName);
12
+ if (idx === -1) return null;
13
+ const value = args[idx + 1];
14
+ return value && !value.startsWith('--') ? value : null;
15
+ }
16
+
17
+ const ROOT = path.resolve(process.env.WF_ROOT || readFlagValue('--root') || path.resolve(__dirname, '..', '..'));
18
+ const VERSION_FILE = path.join(ROOT, 'Harness', '.harness-version');
19
+ const APPLY = args.includes('--apply');
20
+ const JSON_OUT = args.includes('--json');
21
+ const DRY_RUN = args.includes('--dry-run') || (!APPLY && !args.includes('--apply'));
22
+
23
+ const HARNESS_MARKERS = [
24
+ /\bcreate-harness-vibe-coding\b/i,
25
+ /\bproject harness\b/i,
26
+ /\bHarness\/(?:specs|WF|MEMORY|tasks|scripts|subagents|dispatch|context-loading|lifecycle|SETUP)\b/,
27
+ /\bWF-(?:MAX|AUTO|KERNEL|STATE)\b/,
28
+ /^harness:\s*(?:wf-agent|wf-framework|create-harness-vibe-coding)\b/im,
29
+ ];
30
+
31
+ function normalizePath(value) {
32
+ return String(value || '').replace(/\\/g, '/');
33
+ }
34
+
35
+ function sha256(content) {
36
+ return 'sha256-' + createHash('sha256').update(content).digest('hex');
37
+ }
38
+
39
+ function readNormalized(filePath) {
40
+ return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
41
+ }
42
+
43
+ function sha256File(filePath) {
44
+ if (!fs.existsSync(filePath)) return null;
45
+ return sha256(readNormalized(filePath));
46
+ }
47
+
48
+ function containsPath(parent, child) {
49
+ const resolvedParent = path.resolve(parent);
50
+ const resolvedChild = path.resolve(child);
51
+ const rel = path.relative(resolvedParent, resolvedChild);
52
+ return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
53
+ }
54
+
55
+ function resolveUnder(base, rel) {
56
+ if (!rel || typeof rel !== 'string' || path.isAbsolute(rel)) return null;
57
+ const parts = normalizePath(rel).split('/').filter(Boolean);
58
+ if (parts.some(part => part === '..')) return null;
59
+ const resolved = path.resolve(base, ...parts);
60
+ return containsPath(base, resolved) ? resolved : null;
61
+ }
62
+
63
+ function sourceDestForHostFile(host, file) {
64
+ const normalized = normalizePath(file);
65
+ if (host === 'claude') {
66
+ if (normalized === 'settings.json') return '.claude/settings.json';
67
+ if (/^(commands|skills|agents|rules)\//.test(normalized)) return `.claude/${normalized}`;
68
+ }
69
+ if (host === 'codex') {
70
+ if (normalized === 'config.toml') return '.codex/config.toml';
71
+ if (normalized === 'hooks.json') return '.codex/hooks.json';
72
+ if (normalized.startsWith('skills/')) return `.agents/${normalized}`;
73
+ }
74
+ if (host === 'opencode') {
75
+ if (normalized === 'opencode.json') return 'opencode.json';
76
+ if (/^(commands|agents|plugins)\//.test(normalized)) return `.opencode/${normalized}`;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function hasHarnessMarker(filePath) {
82
+ try {
83
+ const body = fs.readFileSync(filePath, 'utf8');
84
+ return HARNESS_MARKERS.some(marker => marker.test(body));
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function readVersion() {
91
+ if (!fs.existsSync(VERSION_FILE)) {
92
+ throw new Error(`Harness/.harness-version not found under ${ROOT}`);
93
+ }
94
+ return JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8'));
95
+ }
96
+
97
+ function hostTargets(version) {
98
+ const hostGlobal = version?.hostGlobal;
99
+ if (!hostGlobal || hostGlobal.copyMode !== 'copy' || !hostGlobal.targets || typeof hostGlobal.targets !== 'object') {
100
+ return null;
101
+ }
102
+ return hostGlobal.targets;
103
+ }
104
+
105
+ function summarize(plan) {
106
+ return {
107
+ created: plan.created.length,
108
+ updated: plan.updated.length,
109
+ conflict: plan.conflict.length,
110
+ skipped: plan.skipped.length,
111
+ errors: plan.errors.length,
112
+ };
113
+ }
114
+
115
+ function createPlan(version) {
116
+ const targets = hostTargets(version);
117
+ const plan = {
118
+ root: normalizePath(ROOT),
119
+ installScope: version?.installScope || 'project',
120
+ created: [],
121
+ updated: [],
122
+ conflict: [],
123
+ skipped: [],
124
+ errors: [],
125
+ };
126
+
127
+ if (version?.installScope !== 'global') {
128
+ plan.skipped.push({ reason: 'not a global install' });
129
+ return plan;
130
+ }
131
+ if (!targets) {
132
+ plan.errors.push({ reason: 'Harness/.harness-version missing hostGlobal copy targets' });
133
+ return plan;
134
+ }
135
+
136
+ for (const [host, target] of Object.entries(targets)) {
137
+ const hostRoot = target && typeof target.root === 'string'
138
+ ? path.resolve(target.root)
139
+ : null;
140
+ const files = Array.isArray(target?.files) ? target.files : [];
141
+ if (!hostRoot) {
142
+ plan.errors.push({ host, reason: 'missing host root' });
143
+ continue;
144
+ }
145
+ for (const file of files) {
146
+ const sourceDest = sourceDestForHostFile(host, file);
147
+ const sourcePath = sourceDest ? resolveUnder(ROOT, sourceDest) : null;
148
+ const targetPath = resolveUnder(hostRoot, file);
149
+ if (!sourceDest || !sourcePath) {
150
+ plan.errors.push({ host, file, reason: 'unknown host-global source mapping' });
151
+ continue;
152
+ }
153
+ if (!targetPath) {
154
+ plan.errors.push({ host, file, source: sourceDest, reason: 'invalid host-global target path' });
155
+ continue;
156
+ }
157
+ if (!fs.existsSync(sourcePath)) {
158
+ plan.errors.push({ host, file, source: sourceDest, reason: 'missing runtime source file' });
159
+ continue;
160
+ }
161
+ if (fs.existsSync(targetPath)) {
162
+ const stat = fs.lstatSync(targetPath);
163
+ if (stat.isSymbolicLink()) {
164
+ plan.errors.push({ host, file, source: sourceDest, reason: 'target is symlink' });
165
+ continue;
166
+ }
167
+ if (!stat.isFile()) {
168
+ plan.errors.push({ host, file, source: sourceDest, reason: 'target is not a regular file' });
169
+ continue;
170
+ }
171
+ }
172
+
173
+ const sourceHash = sha256File(sourcePath);
174
+ const targetHash = sha256File(targetPath);
175
+ const entry = { host, file, source: sourceDest, targetRoot: normalizePath(hostRoot), sourceHash, targetHash };
176
+ if (!targetHash) {
177
+ plan.created.push({ ...entry, reason: 'missing host-global copy' });
178
+ } else if (targetHash === sourceHash) {
179
+ plan.skipped.push({ ...entry, reason: 'already current' });
180
+ } else if (hasHarnessMarker(targetPath)) {
181
+ plan.updated.push({ ...entry, reason: 'stale Harness-marked host-global copy' });
182
+ } else {
183
+ plan.conflict.push({ ...entry, reason: 'existing host-global file lacks Harness ownership marker' });
184
+ }
185
+ }
186
+ }
187
+
188
+ return plan;
189
+ }
190
+
191
+ function applyPlan(plan) {
192
+ const written = [];
193
+ for (const entry of [...plan.created, ...plan.updated]) {
194
+ const sourcePath = resolveUnder(ROOT, entry.source);
195
+ const targetPath = resolveUnder(entry.targetRoot, entry.file);
196
+ if (!sourcePath || !targetPath) {
197
+ plan.errors.push({ ...entry, reason: 'path resolution failed during apply' });
198
+ continue;
199
+ }
200
+ if (fs.existsSync(targetPath) && fs.lstatSync(targetPath).isSymbolicLink()) {
201
+ plan.errors.push({ ...entry, reason: 'target became symlink during apply' });
202
+ continue;
203
+ }
204
+ const content = readNormalized(sourcePath);
205
+ if (sha256(content) !== entry.sourceHash) {
206
+ plan.errors.push({ ...entry, reason: 'runtime source changed during apply' });
207
+ continue;
208
+ }
209
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
210
+ fs.writeFileSync(targetPath, content, 'utf8');
211
+ written.push({ host: entry.host, file: entry.file, source: entry.source });
212
+ }
213
+ return written;
214
+ }
215
+
216
+ function printResult(result) {
217
+ if (JSON_OUT) {
218
+ console.log(JSON.stringify(result, null, 2));
219
+ return;
220
+ }
221
+ if (result.status === 'not-global') {
222
+ console.log('Host-global sync skipped: installScope is not global.');
223
+ return;
224
+ }
225
+ console.log(`Host-global sync ${result.status}: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.conflict} conflict, ${result.summary.errors} error.`);
226
+ if (result.plan?.conflict?.length) {
227
+ for (const entry of result.plan.conflict) {
228
+ console.log(` conflict ${entry.host}:${entry.file} (${entry.reason})`);
229
+ }
230
+ }
231
+ if (result.plan?.errors?.length) {
232
+ for (const entry of result.plan.errors) {
233
+ console.log(` error ${entry.host || 'host'}:${entry.file || ''} ${entry.reason}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ function main() {
239
+ let version;
240
+ try {
241
+ version = readVersion();
242
+ } catch (err) {
243
+ const result = { status: 'error', root: normalizePath(ROOT), message: err.message };
244
+ printResult(result);
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+
249
+ const plan = createPlan(version);
250
+ const summary = summarize(plan);
251
+ let written = [];
252
+ if (APPLY && summary.conflict === 0 && summary.errors === 0) {
253
+ written = applyPlan(plan);
254
+ }
255
+ const afterErrors = plan.errors.length;
256
+ const status = version?.installScope !== 'global'
257
+ ? 'not-global'
258
+ : afterErrors > 0
259
+ ? 'error'
260
+ : summary.conflict > 0
261
+ ? 'conflict'
262
+ : summary.created + summary.updated > 0
263
+ ? (APPLY ? 'synced' : 'needs-sync')
264
+ : 'ok';
265
+ const result = {
266
+ status,
267
+ dryRun: DRY_RUN,
268
+ applied: APPLY && status === 'synced',
269
+ root: normalizePath(ROOT),
270
+ summary: summarize(plan),
271
+ written,
272
+ plan,
273
+ };
274
+ printResult(result);
275
+ if (!['ok', 'synced', 'not-global'].includes(status)) process.exitCode = 1;
276
+ }
277
+
278
+ main();