@walwal-harness/cli 7.1.46 → 7.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/apps/harness-dashboard/components/brick/reports.tsx +13 -4
- package/apps/harness-dashboard/components/brick/views-timeline.tsx +64 -76
- package/apps/harness-dashboard/lib/brick/adapter.ts +12 -0
- package/apps/harness-dashboard/lib/brick/contract.ts +13 -2
- package/assets/templates/gitignore-append.txt +26 -6
- package/bin/init.js +98 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-up.sh +15 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ docmeta:
|
|
|
4
4
|
title: walwal-harness CHANGELOG
|
|
5
5
|
type: output
|
|
6
6
|
createdAt: 2026-05-07T00:00:00Z
|
|
7
|
-
updatedAt: 2026-
|
|
7
|
+
updatedAt: 2026-06-05T00:00:00Z
|
|
8
8
|
source:
|
|
9
9
|
producer: agent
|
|
10
10
|
skillId: harness-release
|
|
@@ -31,6 +31,11 @@ docmeta:
|
|
|
31
31
|
|
|
32
32
|
## Unreleased
|
|
33
33
|
|
|
34
|
+
## 7.1.48 — Auto-managed `.harness` `.gitignore` + runtime untrack on init (2026-06-05)
|
|
35
|
+
- `walwal-harness init` now writes/refreshes a marked managed block in the project-root `.gitignore` so machine-specific harness runtime/local state is never committed and never pollutes teammates' diffs. Ignored: live conductor `progress.json`/`progress.log`/`handoff.json`/`events.jsonl`, the `todos/` live queue (`state.json` + `events.jsonl`), `actions/`, `ops/`, `activity/`, `logs/`, `archive/` migration backups, and the `dashboard/` visualization tooling. Durable team knowledge stays tracked: `conventions/`, `gotchas/`, `documents/`, `memories/`, `memory.md`, `shared/`, `config.json`, `HARNESS.md`, `.bundle-version`.
|
|
36
|
+
- Init also best-effort `git rm --cached`s any of those runtime paths committed before the ignore block existed (files stay on disk), fixing the "`.gitignore` added but already-tracked files keep showing as changes" case.
|
|
37
|
+
- Canonical ignore list now lives in `assets/templates/gitignore-append.txt` as a single source of truth, consumed by both `bin/init.js` and `scripts/harness-dashboard-up.sh` (the dashboard launcher defers to init's managed block instead of appending its own narrow one).
|
|
38
|
+
|
|
34
39
|
## 7.1.38 — CEO approval authority for routine operations (2026-05-31)
|
|
35
40
|
|
|
36
41
|
- CEO now has explicit authority to approve reversible routine operations without Owner confirmation, including mission consolidation/supersede cleanup, monitoring cadence, briefing templates, local cron/launchd/wake automation, dashboard refresh, and Telegram/dashboard notifications that use already configured credentials.
|
|
@@ -172,23 +172,32 @@ export function buildReport(target: DocTarget, state: ContractState): BuiltRepor
|
|
|
172
172
|
const agent =
|
|
173
173
|
state.agents.find((a) => a.id === target.agent) ?? state.agents[0];
|
|
174
174
|
|
|
175
|
+
// When opened from a timeline activity block, prefix the report with the
|
|
176
|
+
// moment (time + mission) the clicked block represents.
|
|
177
|
+
const moment =
|
|
178
|
+
target.at != null
|
|
179
|
+
? `**Activity @ ${new Date(target.at).toLocaleString()}**` +
|
|
180
|
+
(target.mission ? ` · mission \`${target.mission}\`` : "") +
|
|
181
|
+
`\n\n---\n\n`
|
|
182
|
+
: "";
|
|
183
|
+
|
|
175
184
|
if (target.type === "worker") {
|
|
176
185
|
const worker = target.worker;
|
|
177
|
-
const body = stripFrontmatter(worker.report) || NO_WORKER_SOURCE;
|
|
186
|
+
const body = moment + (stripFrontmatter(worker.report) || NO_WORKER_SOURCE);
|
|
178
187
|
return {
|
|
179
188
|
title: worker.name,
|
|
180
189
|
owner: agent,
|
|
181
|
-
kind: "Worker Brief",
|
|
190
|
+
kind: target.at != null ? "Worker · moment" : "Worker Brief",
|
|
182
191
|
status: worker.status,
|
|
183
192
|
body,
|
|
184
193
|
};
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
const body = stripFrontmatter(agent.report) || NO_SOURCE;
|
|
196
|
+
const body = moment + (stripFrontmatter(agent.report) || NO_SOURCE);
|
|
188
197
|
return {
|
|
189
198
|
title: agent.work,
|
|
190
199
|
owner: agent,
|
|
191
|
-
kind: "CXX Document",
|
|
200
|
+
kind: target.at != null ? "CXX · moment" : "CXX Document",
|
|
192
201
|
status: agent.status,
|
|
193
202
|
body,
|
|
194
203
|
};
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
/* =============================================================
|
|
4
4
|
walwal-harness · VIEW 3 — TIMELINE (cadence heatmap + swimlanes)
|
|
5
|
-
Ported from harness/views-timeline.jsx (design handoff)
|
|
5
|
+
Ported from harness/views-timeline.jsx (design handoff), wired to REAL
|
|
6
|
+
per-lane activity marks (s.marks) instead of the prototype's random marks.
|
|
6
7
|
Exports: TimelineView, Cadence
|
|
7
8
|
============================================================= */
|
|
8
9
|
|
|
9
|
-
import React
|
|
10
|
+
import React from "react";
|
|
10
11
|
import type { CSSProperties } from "react";
|
|
11
12
|
|
|
12
13
|
import type {
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
ContractState,
|
|
15
16
|
ContractWorker,
|
|
16
17
|
DocTarget,
|
|
18
|
+
TimelineMark,
|
|
17
19
|
} from "../../lib/brick/contract";
|
|
18
20
|
import { StatusDot, Label } from "./ui";
|
|
19
21
|
|
|
@@ -66,104 +68,92 @@ export interface TimelineViewProps {
|
|
|
66
68
|
openDoc?: (target: DocTarget) => void;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
type SwimMark = { ago: number; dur: number };
|
|
70
|
-
|
|
71
71
|
type SwimRow = {
|
|
72
72
|
kind: "exec" | "worker";
|
|
73
73
|
id: string;
|
|
74
|
+
/** activity lane id: "cto" for an exec, "cto:worker-name" for a worker. */
|
|
75
|
+
laneId: string;
|
|
74
76
|
label: string;
|
|
75
77
|
hue: string;
|
|
76
78
|
agent: AgentRole;
|
|
77
79
|
status: string;
|
|
78
80
|
progress?: number | null;
|
|
81
|
+
worker?: ContractWorker;
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
export function TimelineView({ s, openDoc }: TimelineViewProps) {
|
|
82
|
-
// stable seeded marks per row id
|
|
83
|
-
const markCache = useRef<Record<string, SwimMark[]>>({});
|
|
84
|
-
function seedMarks(id: string, density: number): SwimMark[] {
|
|
85
|
-
const cached = markCache.current[id];
|
|
86
|
-
if (cached) return cached;
|
|
87
|
-
const arr: SwimMark[] = [];
|
|
88
|
-
const n = Math.floor(density * (4 + Math.random() * 7));
|
|
89
|
-
for (let i = 0; i < n; i++) {
|
|
90
|
-
arr.push({ ago: Math.random() * SPAN_MS, dur: 40000 + Math.random() * 600000 });
|
|
91
|
-
}
|
|
92
|
-
markCache.current[id] = arr;
|
|
93
|
-
return arr;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// build rows: exec + its workers
|
|
97
85
|
const order: AgentRole[] = ["ceo", "coo", "cto", "cqo", "cdo", "ops"];
|
|
86
|
+
|
|
87
|
+
// Build rows: each exec + the real WORKERS under it. Worker rows come from the
|
|
88
|
+
// activity sub-lanes ("cxx:worker-name") and the active-mission workers — NOT
|
|
89
|
+
// from missions — so a CXX lists the workers that actually ran under it.
|
|
98
90
|
const rows: SwimRow[] = [];
|
|
99
|
-
order.forEach((
|
|
100
|
-
const a = s.agents.find((x) => x.id ===
|
|
91
|
+
order.forEach((role) => {
|
|
92
|
+
const a = s.agents.find((x) => x.id === role);
|
|
101
93
|
if (!a) return;
|
|
102
|
-
rows.push({ kind: "exec", id:
|
|
103
|
-
s.workers
|
|
104
|
-
.filter((w) => w.agent === id)
|
|
105
|
-
.forEach((w) => {
|
|
106
|
-
rows.push({
|
|
107
|
-
kind: "worker",
|
|
108
|
-
id: w.id,
|
|
109
|
-
label: w.name,
|
|
110
|
-
hue: a.hue,
|
|
111
|
-
agent: a.id,
|
|
112
|
-
status: w.status,
|
|
113
|
-
progress: w.progress,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
94
|
+
rows.push({ kind: "exec", id: role, laneId: role, label: a.name, hue: a.hue, agent: role, status: a.status });
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
96
|
+
const lanes = new Map<string, { label: string; worker?: ContractWorker }>();
|
|
97
|
+
for (const laneId of Object.keys(s.marks)) {
|
|
98
|
+
if (laneId.startsWith(role + ":")) {
|
|
99
|
+
lanes.set(laneId, { label: laneId.slice(role.length + 1) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// active-mission workers (carry a report + status); key matches the sub-lane id.
|
|
103
|
+
for (const w of s.workers) {
|
|
104
|
+
if (w.agent === role) lanes.set(w.id, { label: w.name, worker: w });
|
|
105
|
+
}
|
|
106
|
+
for (const [laneId, info] of lanes) {
|
|
107
|
+
rows.push({
|
|
108
|
+
kind: "worker",
|
|
109
|
+
id: laneId,
|
|
110
|
+
laneId,
|
|
111
|
+
label: info.label,
|
|
112
|
+
hue: a.hue,
|
|
113
|
+
agent: role,
|
|
114
|
+
status: info.worker?.status ?? "idle",
|
|
115
|
+
progress: info.worker?.progress ?? null,
|
|
116
|
+
worker: info.worker,
|
|
117
|
+
});
|
|
125
118
|
}
|
|
126
119
|
});
|
|
127
120
|
|
|
128
121
|
const xOf = (ago: number): number => Math.max(0, Math.min(100, (ago / SPAN_MS) * 100));
|
|
129
|
-
|
|
130
|
-
// axis ticks
|
|
131
122
|
const ticks: number[] = [];
|
|
132
123
|
for (let h = 0; h <= 12; h += 2) ticks.push(h);
|
|
124
|
+
const fmtAt = (at: number): string => {
|
|
125
|
+
const d = new Date(at);
|
|
126
|
+
return Number.isFinite(d.getTime()) ? d.toTimeString().slice(0, 8) : "?";
|
|
127
|
+
};
|
|
133
128
|
|
|
129
|
+
// Label click → the whole row's report.
|
|
134
130
|
const openRow = (r: SwimRow) => {
|
|
135
131
|
if (!openDoc) return;
|
|
136
|
-
if (r.
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const worker: ContractWorker | undefined = s.workers.find((w) => w.id === r.id);
|
|
141
|
-
if (worker) openDoc({ type: "worker", agent: r.agent, worker });
|
|
132
|
+
if (r.worker) openDoc({ type: "worker", agent: r.agent, worker: r.worker });
|
|
133
|
+
else openDoc({ type: "agent", agent: r.agent });
|
|
142
134
|
};
|
|
135
|
+
// Block click → detail for that one activity mark (what the lane did at that moment).
|
|
136
|
+
const openMark = (r: SwimRow, m: TimelineMark, e: React.MouseEvent) => {
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
if (!openDoc) return;
|
|
139
|
+
if (r.worker) openDoc({ type: "worker", agent: r.agent, worker: r.worker, at: m.at, mission: m.mission });
|
|
140
|
+
else openDoc({ type: "agent", agent: r.agent, at: m.at, mission: m.mission });
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const workerCount = rows.filter((r) => r.kind === "worker").length;
|
|
143
144
|
|
|
144
145
|
return (
|
|
145
146
|
<div className="timelineview">
|
|
146
147
|
<Cadence cadence={s.cadence} />
|
|
147
148
|
<div className="swim">
|
|
148
|
-
<Label
|
|
149
|
-
right={
|
|
150
|
-
rows.filter((r) => r.kind === "worker").length +
|
|
151
|
-
" workers · " +
|
|
152
|
-
order.length +
|
|
153
|
-
" execs"
|
|
154
|
-
}
|
|
155
|
-
>
|
|
149
|
+
<Label right={workerCount + " workers · " + order.length + " execs"}>
|
|
156
150
|
ORCHESTRATION TIMELINE · now → −12h
|
|
157
151
|
</Label>
|
|
158
152
|
<div className="swim-axis">
|
|
159
153
|
<div className="swim-axis-pad" />
|
|
160
154
|
<div className="swim-axis-track">
|
|
161
155
|
{ticks.map((h) => (
|
|
162
|
-
<span
|
|
163
|
-
key={h}
|
|
164
|
-
className="swim-tick"
|
|
165
|
-
style={{ left: (h / 12) * 100 + "%" }}
|
|
166
|
-
>
|
|
156
|
+
<span key={h} className="swim-tick" style={{ left: (h / 12) * 100 + "%" }}>
|
|
167
157
|
{h === 0 ? "now" : "−" + h + "h"}
|
|
168
158
|
</span>
|
|
169
159
|
))}
|
|
@@ -171,8 +161,9 @@ export function TimelineView({ s, openDoc }: TimelineViewProps) {
|
|
|
171
161
|
</div>
|
|
172
162
|
<div className="swim-rows">
|
|
173
163
|
{rows.map((r) => {
|
|
174
|
-
const marks =
|
|
175
|
-
|
|
164
|
+
const marks = (s.marks[r.laneId] ?? []).filter(
|
|
165
|
+
(m) => s.now - m.at >= 0 && s.now - m.at <= SPAN_MS
|
|
166
|
+
);
|
|
176
167
|
return (
|
|
177
168
|
<div
|
|
178
169
|
key={r.id}
|
|
@@ -205,21 +196,18 @@ export function TimelineView({ s, openDoc }: TimelineViewProps) {
|
|
|
205
196
|
<span
|
|
206
197
|
key={i}
|
|
207
198
|
className="swim-block"
|
|
199
|
+
role="button"
|
|
200
|
+
title={fmtAt(m.at) + (m.mission ? " · " + m.mission : "")}
|
|
201
|
+
onClick={(e) => openMark(r, m, e)}
|
|
208
202
|
style={{
|
|
209
|
-
left: xOf(m.
|
|
210
|
-
width:
|
|
203
|
+
left: xOf(s.now - m.at) + "%",
|
|
204
|
+
width: "9px",
|
|
205
|
+
cursor: "pointer",
|
|
211
206
|
background: r.hue,
|
|
212
|
-
opacity: r.kind === "exec" ? 0.85 : 0.
|
|
207
|
+
opacity: r.kind === "exec" ? 0.85 : 0.6,
|
|
213
208
|
}}
|
|
214
209
|
/>
|
|
215
210
|
))}
|
|
216
|
-
{live.map((age, i) => (
|
|
217
|
-
<span
|
|
218
|
-
key={"l" + i}
|
|
219
|
-
className="swim-live"
|
|
220
|
-
style={{ left: xOf(age) + "%", background: r.hue }}
|
|
221
|
-
/>
|
|
222
|
-
))}
|
|
223
211
|
{r.kind === "worker" && r.progress != null && (
|
|
224
212
|
<span
|
|
225
213
|
className="swim-now-prog"
|
|
@@ -284,6 +284,17 @@ export function toContractState(
|
|
|
284
284
|
eventsTotal: snapshot.events.length,
|
|
285
285
|
};
|
|
286
286
|
|
|
287
|
+
// ---- timeline marks: real per-lane activity (epoch + mission) ----------
|
|
288
|
+
const marks: Record<string, Array<{ at: number; mission: string | null }>> = {};
|
|
289
|
+
for (const sample of snapshot.activitySamples) {
|
|
290
|
+
const at = parseTs(sample.ts);
|
|
291
|
+
if (at == null) continue;
|
|
292
|
+
(marks[sample.laneId] = marks[sample.laneId] ?? []).push({
|
|
293
|
+
at,
|
|
294
|
+
mission: sample.missionId ?? null,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
287
298
|
return {
|
|
288
299
|
now: nowMs,
|
|
289
300
|
agents,
|
|
@@ -295,6 +306,7 @@ export function toContractState(
|
|
|
295
306
|
metrics,
|
|
296
307
|
mtrend: [],
|
|
297
308
|
ctrend: [],
|
|
309
|
+
marks,
|
|
298
310
|
};
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -39,8 +39,8 @@ export type AlertLevel = "stale" | "attention";
|
|
|
39
39
|
|
|
40
40
|
/** A reference to a document the DocViewer can render. */
|
|
41
41
|
export type DocTarget =
|
|
42
|
-
| { type: "agent"; agent: AgentRole }
|
|
43
|
-
| { type: "worker"; agent: AgentRole; worker: ContractWorker };
|
|
42
|
+
| { type: "agent"; agent: AgentRole; at?: number; mission?: string | null }
|
|
43
|
+
| { type: "worker"; agent: AgentRole; worker: ContractWorker; at?: number; mission?: string | null };
|
|
44
44
|
|
|
45
45
|
/** Static, design-fixed definition of one exec agent. */
|
|
46
46
|
export interface AgentDef {
|
|
@@ -173,6 +173,17 @@ export interface ContractState {
|
|
|
173
173
|
mtrend: number[];
|
|
174
174
|
/** cost sparkline samples; [] when no source. */
|
|
175
175
|
ctrend: number[];
|
|
176
|
+
/** Real per-lane activity marks (epoch ms + mission) for the timeline
|
|
177
|
+
* swimlanes, keyed by laneId ("cto" exec lane or "cto:worker-name" sub-lane). */
|
|
178
|
+
marks: Record<string, TimelineMark[]>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** One real activity mark on a timeline lane (from an activity sample). */
|
|
182
|
+
export interface TimelineMark {
|
|
183
|
+
/** epoch ms of the activity. */
|
|
184
|
+
at: number;
|
|
185
|
+
/** mission the activity belonged to, if known. */
|
|
186
|
+
mission: string | null;
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
/* ---- agent (exec) definitions -----------------------------------------
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
# walwal-harness
|
|
2
|
-
.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.harness/
|
|
6
|
-
.harness/
|
|
1
|
+
# >>> walwal-harness managed (local/runtime state — do not edit between markers) >>>
|
|
2
|
+
# Auto-managed by `walwal-harness init` (refreshed on every init).
|
|
3
|
+
# Edit the package template assets/templates/gitignore-append.txt, not this block.
|
|
4
|
+
#
|
|
5
|
+
# SHARED with teammates (kept in git): .harness/{conventions,gotchas,documents,
|
|
6
|
+
# memories,shared}/ .harness/{memory.md,config.json,HARNESS.md,.bundle-version}
|
|
7
|
+
# Everything listed below is machine-specific runtime or local tooling — never shared.
|
|
8
|
+
#
|
|
9
|
+
# Live conductor todo-queue + transition log (heartbeats churn each tick; embeds local paths)
|
|
10
|
+
.harness/todos/state.json
|
|
11
|
+
.harness/todos/events.jsonl
|
|
12
|
+
# Live conductor / handoff / event state (rewritten every run)
|
|
13
|
+
.harness/progress.json
|
|
14
|
+
.harness/progress.log
|
|
15
|
+
.harness/handoff.json
|
|
16
|
+
.harness/events.jsonl
|
|
17
|
+
# Active-sprint, service-ops, activity heatmap & logs runtime
|
|
18
|
+
.harness/actions/
|
|
19
|
+
.harness/ops/
|
|
20
|
+
.harness/activity/
|
|
21
|
+
.harness/logs/
|
|
22
|
+
# Migration & pre-harness backup snapshots (local-only)
|
|
23
|
+
.harness/archive/
|
|
24
|
+
# Brick Office dashboard — local visualization tool + per-env data & build artifacts
|
|
25
|
+
.harness/dashboard/
|
|
26
|
+
# <<< walwal-harness managed <<<
|
package/bin/init.js
CHANGED
|
@@ -853,6 +853,102 @@ function ensureHarnessEnv() {
|
|
|
853
853
|
log('.env: HARNESS_BASE_PORT placeholder added — CEO/CXX must choose an available value before starting services');
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
+
// ── .gitignore management ───────────────────────────────────────────────────
|
|
857
|
+
// Keep the host project's root .gitignore in sync with the harness's local/runtime
|
|
858
|
+
// state so high-churn conductor files, backups, and dashboard tooling never get
|
|
859
|
+
// committed (and pollute teammates' diffs). The canonical entry list ships in
|
|
860
|
+
// assets/templates/gitignore-append.txt — single source of truth, also consumed by
|
|
861
|
+
// scripts/harness-dashboard-up.sh.
|
|
862
|
+
const GITIGNORE_MARKER_BEGIN = '# >>> walwal-harness managed';
|
|
863
|
+
const GITIGNORE_MARKER_END = '# <<< walwal-harness managed';
|
|
864
|
+
|
|
865
|
+
function readGitignoreBlock() {
|
|
866
|
+
const tplPath = path.join(PKG_ROOT, 'assets', 'templates', 'gitignore-append.txt');
|
|
867
|
+
if (!fileExists(tplPath)) return null;
|
|
868
|
+
const block = fs.readFileSync(tplPath, 'utf8').replace(/\s+$/, '');
|
|
869
|
+
return block || null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Non-comment, non-empty lines of the managed block = the local/runtime pathspecs.
|
|
873
|
+
function localPathsFromBlock(block) {
|
|
874
|
+
return block
|
|
875
|
+
.split('\n')
|
|
876
|
+
.map((l) => l.trim())
|
|
877
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Write or refresh the marked managed block in the project-root .gitignore.
|
|
881
|
+
// Idempotent: an existing block is replaced in place; everything else is preserved.
|
|
882
|
+
function ensureProjectGitignore() {
|
|
883
|
+
const block = readGitignoreBlock();
|
|
884
|
+
if (!block) {
|
|
885
|
+
log(' → .gitignore: skipped (managed template missing)');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
|
|
889
|
+
const original = fileExists(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
890
|
+
const lines = original.length ? original.split('\n') : [];
|
|
891
|
+
|
|
892
|
+
const beginLine = lines.findIndex((l) => l.startsWith(GITIGNORE_MARKER_BEGIN));
|
|
893
|
+
let next;
|
|
894
|
+
if (beginLine !== -1) {
|
|
895
|
+
let endLine = lines.findIndex((l, i) => i >= beginLine && l.startsWith(GITIGNORE_MARKER_END));
|
|
896
|
+
if (endLine === -1) endLine = lines.length - 1; // tolerate a truncated block
|
|
897
|
+
next = [...lines.slice(0, beginLine), ...block.split('\n'), ...lines.slice(endLine + 1)].join('\n');
|
|
898
|
+
} else {
|
|
899
|
+
const head = original.replace(/\n*$/, '');
|
|
900
|
+
next = (head ? head + '\n\n' : '') + block + '\n';
|
|
901
|
+
}
|
|
902
|
+
if (!next.endsWith('\n')) next += '\n';
|
|
903
|
+
if (next !== original) {
|
|
904
|
+
fs.writeFileSync(gitignorePath, next);
|
|
905
|
+
log(beginLine !== -1
|
|
906
|
+
? ' → .gitignore: refreshed walwal-harness managed block'
|
|
907
|
+
: ' → .gitignore: added walwal-harness runtime/local ignore block');
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Best-effort: a project that committed runtime state before the ignore block existed
|
|
912
|
+
// keeps showing churn until those paths leave the index. Drop them from tracking
|
|
913
|
+
// (files stay on disk). Never fail init over git: a non-repo / missing-git / odd state
|
|
914
|
+
// is a quiet no-op.
|
|
915
|
+
function gitUntrackLocalState() {
|
|
916
|
+
const block = readGitignoreBlock();
|
|
917
|
+
if (!block) return;
|
|
918
|
+
try {
|
|
919
|
+
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
|
920
|
+
cwd: PROJECT_ROOT, stdio: ['ignore', 'pipe', 'ignore'],
|
|
921
|
+
}).toString().trim();
|
|
922
|
+
if (inside !== 'true') return;
|
|
923
|
+
} catch {
|
|
924
|
+
return; // not a git repo / git unavailable
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const removed = [];
|
|
928
|
+
for (const rel of localPathsFromBlock(block)) {
|
|
929
|
+
let tracked = '';
|
|
930
|
+
try {
|
|
931
|
+
tracked = execSync(`git ls-files -- ${shellQuote(rel)}`, {
|
|
932
|
+
cwd: PROJECT_ROOT, stdio: ['ignore', 'pipe', 'ignore'],
|
|
933
|
+
}).toString().trim();
|
|
934
|
+
} catch {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!tracked) continue;
|
|
938
|
+
try {
|
|
939
|
+
execSync(`git rm -r --cached --quiet --ignore-unmatch -- ${shellQuote(rel)}`, {
|
|
940
|
+
cwd: PROJECT_ROOT, stdio: ['ignore', 'ignore', 'ignore'],
|
|
941
|
+
});
|
|
942
|
+
removed.push(rel);
|
|
943
|
+
} catch {
|
|
944
|
+
// leave it tracked; non-fatal
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (removed.length) {
|
|
948
|
+
log(` → git: untracked ${removed.length} now-ignored runtime path(s), kept on disk (${removed.join(', ')})`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
856
952
|
function ensureStructuredRuntimeFiles({ dryRun = false } = {}) {
|
|
857
953
|
const todosDir = path.join(HARNESS_DIR, 'todos');
|
|
858
954
|
const statePath = path.join(todosDir, 'state.json');
|
|
@@ -2621,6 +2717,8 @@ function main() {
|
|
|
2621
2717
|
console.log('');
|
|
2622
2718
|
|
|
2623
2719
|
scaffoldHarness();
|
|
2720
|
+
ensureProjectGitignore();
|
|
2721
|
+
gitUntrackLocalState();
|
|
2624
2722
|
installSkills();
|
|
2625
2723
|
installScripts();
|
|
2626
2724
|
ensureWakeScheduler();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.48",
|
|
4
4
|
"description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -53,22 +53,27 @@ fi
|
|
|
53
53
|
|
|
54
54
|
ensure_gitignore() {
|
|
55
55
|
local gitignore="${HARNESS_ROOT}/.gitignore"
|
|
56
|
-
|
|
56
|
+
# `walwal-harness init` owns the canonical managed block (covers .harness/dashboard/
|
|
57
|
+
# plus all other runtime/local state). If it's already present, nothing to add.
|
|
58
|
+
local managed_marker="# >>> walwal-harness managed"
|
|
59
|
+
if [[ -f "$gitignore" ]] && grep -Fq "$managed_marker" "$gitignore"; then
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
# Prefer the package's canonical template (single source of truth shared with init.js);
|
|
63
|
+
# fall back to a dashboard-only block for older packages that lack the template.
|
|
64
|
+
local tpl="${PACKAGE_ROOT}/assets/templates/gitignore-append.txt"
|
|
57
65
|
local block
|
|
58
|
-
|
|
66
|
+
if [[ -f "$tpl" ]]; then
|
|
67
|
+
block="$(cat "$tpl")"
|
|
68
|
+
else
|
|
69
|
+
block="$(cat <<'EOF'
|
|
59
70
|
# walwal-harness dashboard runtime
|
|
60
|
-
.harness/dashboard/
|
|
61
|
-
.harness/dashboard/apps/harness-dashboard/.next/
|
|
62
|
-
.harness/dashboard/apps/harness-dashboard/.turbo/
|
|
63
|
-
.harness/dashboard/apps/harness-dashboard/tsconfig.tsbuildinfo
|
|
64
|
-
.harness/dashboard/.download/
|
|
71
|
+
.harness/dashboard/
|
|
65
72
|
EOF
|
|
66
73
|
)"
|
|
67
|
-
if [[ -f "$gitignore" ]] && grep -Fq "$marker" "$gitignore"; then
|
|
68
|
-
return
|
|
69
74
|
fi
|
|
70
75
|
mkdir -p "$(dirname "$gitignore")"
|
|
71
|
-
if [[ -s "$gitignore" ]] && [[ "$(tail -c 1 "$gitignore" 2>/dev/null || true)" != "" ]]; then
|
|
76
|
+
if [[ -f "$gitignore" ]] && [[ -s "$gitignore" ]] && [[ "$(tail -c 1 "$gitignore" 2>/dev/null || true)" != "" ]]; then
|
|
72
77
|
printf '\n' >> "$gitignore"
|
|
73
78
|
fi
|
|
74
79
|
printf '%s\n' "$block" >> "$gitignore"
|