@osfactory/har 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +81 -9
- package/dist/prompts/system-authoring.md +26 -0
- package/dist/templates/adaptation-prompt-init.md +37 -1
- package/dist/templates/adaptation-prompt-maintain.md +30 -0
- package/dist/templates/har-boilerplate/CLAUDE.agent.md +14 -0
- package/dist/templates/har-boilerplate/README.md +18 -1
- package/dist/templates/har-boilerplate/agent-cli.sh +7 -1
- package/dist/templates/har-boilerplate/agent-slot.sh +60 -7
- package/dist/templates/har-boilerplate/harness.env +15 -0
- package/dist/templates/har-boilerplate/launch.sh +50 -0
- package/dist/templates/har-boilerplate/stages.json +1 -1
- package/dist/templates/har-boilerplate/verify.sh +9 -0
- package/dist/templates/har-boilerplate-cli/CLAUDE.agent.md +8 -0
- package/dist/templates/har-boilerplate-cli/README.md +6 -1
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +60 -7
- package/dist/templates/har-boilerplate-cli/harness.env +8 -0
- package/dist/templates/har-boilerplate-cli/launch.sh +41 -0
- package/dist/templates/har-boilerplate-cli/stages.json +1 -1
- package/dist/templates/har-boilerplate-cli/verify.sh +9 -0
- package/dist/templates/har-boilerplate-ios/CLAUDE.agent.md +8 -0
- package/dist/templates/har-boilerplate-ios/README.md +5 -1
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +60 -7
- package/dist/templates/har-boilerplate-ios/harness.env +4 -0
- package/dist/templates/har-boilerplate-ios/launch.sh +41 -0
- package/dist/templates/har-boilerplate-ios/stages.json +1 -1
- package/dist/templates/har-boilerplate-ios/verify.sh +10 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22989,7 +22989,8 @@ var SlotRegistryEntrySchema = external_exports.object({
|
|
|
22989
22989
|
purpose: external_exports.string().optional(),
|
|
22990
22990
|
ports: external_exports.record(external_exports.number()).optional(),
|
|
22991
22991
|
previewUrls: external_exports.record(external_exports.string()).optional(),
|
|
22992
|
-
status: external_exports.enum(["active", "completed"]).default("active")
|
|
22992
|
+
status: external_exports.enum(["starting", "active", "failed", "completed"]).default("active"),
|
|
22993
|
+
lastError: external_exports.string().optional()
|
|
22993
22994
|
}).passthrough();
|
|
22994
22995
|
var AgentSlotHarnessUsageSchema = external_exports.enum([
|
|
22995
22996
|
"mcp",
|
|
@@ -23016,6 +23017,8 @@ var AgentSlotStatusSchema = external_exports.object({
|
|
|
23016
23017
|
baseCommit: external_exports.string().optional(),
|
|
23017
23018
|
sessionCreatedAt: external_exports.string().optional(),
|
|
23018
23019
|
purpose: external_exports.string().optional(),
|
|
23020
|
+
sessionStatus: external_exports.enum(["starting", "active", "failed", "completed"]).optional(),
|
|
23021
|
+
lastError: external_exports.string().optional(),
|
|
23019
23022
|
/** Worktree checked out to no branch (legacy failure mode; should not happen with sessions). */
|
|
23020
23023
|
detachedHead: external_exports.boolean().optional(),
|
|
23021
23024
|
/** Worktree has uncommitted changes. */
|
|
@@ -27864,10 +27867,11 @@ function listRegisteredRepos() {
|
|
|
27864
27867
|
var fs23 = __toESM(require("fs"));
|
|
27865
27868
|
var os4 = __toESM(require("os"));
|
|
27866
27869
|
var path21 = __toESM(require("path"));
|
|
27867
|
-
var
|
|
27870
|
+
var import_child_process3 = require("child_process");
|
|
27868
27871
|
|
|
27869
27872
|
// src/core/runs.ts
|
|
27870
27873
|
var crypto2 = __toESM(require("crypto"));
|
|
27874
|
+
var import_child_process2 = require("child_process");
|
|
27871
27875
|
var fs22 = __toESM(require("fs"));
|
|
27872
27876
|
var os3 = __toESM(require("os"));
|
|
27873
27877
|
var path20 = __toESM(require("path"));
|
|
@@ -27895,6 +27899,34 @@ function readSlotRegistry(repoPath, agentId) {
|
|
|
27895
27899
|
|
|
27896
27900
|
// src/core/runs.ts
|
|
27897
27901
|
var RUNS_DIR = "runs";
|
|
27902
|
+
function gitCommonDir(cwd) {
|
|
27903
|
+
try {
|
|
27904
|
+
const out = (0, import_child_process2.execSync)("git rev-parse --git-common-dir", {
|
|
27905
|
+
cwd,
|
|
27906
|
+
encoding: "utf8",
|
|
27907
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
27908
|
+
}).trim();
|
|
27909
|
+
return out ? path20.resolve(cwd, out) : void 0;
|
|
27910
|
+
} catch {
|
|
27911
|
+
return void 0;
|
|
27912
|
+
}
|
|
27913
|
+
}
|
|
27914
|
+
function sameGitCheckout(a2, b2) {
|
|
27915
|
+
const left2 = gitCommonDir(a2);
|
|
27916
|
+
const right2 = gitCommonDir(b2);
|
|
27917
|
+
return left2 !== void 0 && right2 !== void 0 && left2 === right2;
|
|
27918
|
+
}
|
|
27919
|
+
function gitPrefix(cwd) {
|
|
27920
|
+
try {
|
|
27921
|
+
return (0, import_child_process2.execSync)("git rev-parse --show-prefix", {
|
|
27922
|
+
cwd,
|
|
27923
|
+
encoding: "utf8",
|
|
27924
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
27925
|
+
}).trim();
|
|
27926
|
+
} catch {
|
|
27927
|
+
return "";
|
|
27928
|
+
}
|
|
27929
|
+
}
|
|
27898
27930
|
function getRunsDir(harnessRoot) {
|
|
27899
27931
|
return path20.join(getHarnessDir(harnessRoot), RUNS_DIR);
|
|
27900
27932
|
}
|
|
@@ -27951,9 +27983,22 @@ function resolveAgentWorkDir(harnessRoot, agentId) {
|
|
|
27951
27983
|
const env3 = readHarnessEnv(harnessRoot);
|
|
27952
27984
|
const projectName = env3.HARNESS_PROJECT_NAME ?? path20.basename(harnessRoot);
|
|
27953
27985
|
const worktreeDir = path20.join(os3.homedir(), "worktrees", `${projectName}-agent-${agentId}`);
|
|
27986
|
+
const relPrefix = gitPrefix(harnessRoot);
|
|
27987
|
+
const sessionEnvFiles = [];
|
|
27988
|
+
const worktreesRoot = path20.join(os3.homedir(), "worktrees");
|
|
27989
|
+
if (fs22.existsSync(worktreesRoot)) {
|
|
27990
|
+
const suffix = `-har-agent-${agentId}-`;
|
|
27991
|
+
for (const entry2 of fs22.readdirSync(worktreesRoot, { withFileTypes: true })) {
|
|
27992
|
+
if (!entry2.isDirectory() || !entry2.name.includes(suffix)) continue;
|
|
27993
|
+
const sessionDir = path20.join(worktreesRoot, entry2.name);
|
|
27994
|
+
if (!sameGitCheckout(harnessRoot, sessionDir)) continue;
|
|
27995
|
+
sessionEnvFiles.push(path20.join(sessionDir, relPrefix, `.env.agent.${agentId}`));
|
|
27996
|
+
}
|
|
27997
|
+
}
|
|
27954
27998
|
const candidates = [
|
|
27955
27999
|
path20.join(worktreeDir, `.env.agent.${agentId}`),
|
|
27956
|
-
path20.join(harnessRoot, `.env.agent.${agentId}`)
|
|
28000
|
+
path20.join(harnessRoot, `.env.agent.${agentId}`),
|
|
28001
|
+
...sessionEnvFiles.sort()
|
|
27957
28002
|
];
|
|
27958
28003
|
for (const envFile of candidates) {
|
|
27959
28004
|
if (!fs22.existsSync(envFile)) continue;
|
|
@@ -28040,7 +28085,7 @@ function listRuns(repoPath, filter = {}) {
|
|
|
28040
28085
|
var BYPASS_WARNING_MS = 15 * 60 * 1e3;
|
|
28041
28086
|
function readGitRemote(repoPath) {
|
|
28042
28087
|
try {
|
|
28043
|
-
const remote = (0,
|
|
28088
|
+
const remote = (0, import_child_process3.execSync)("git remote get-url origin", {
|
|
28044
28089
|
cwd: repoPath,
|
|
28045
28090
|
encoding: "utf8",
|
|
28046
28091
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -28052,7 +28097,7 @@ function readGitRemote(repoPath) {
|
|
|
28052
28097
|
}
|
|
28053
28098
|
function runGit(cwd, args) {
|
|
28054
28099
|
try {
|
|
28055
|
-
return (0,
|
|
28100
|
+
return (0, import_child_process3.execSync)(`git ${args}`, {
|
|
28056
28101
|
cwd,
|
|
28057
28102
|
encoding: "utf8",
|
|
28058
28103
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -28064,6 +28109,29 @@ function runGit(cwd, args) {
|
|
|
28064
28109
|
function readWorktreeBranch(worktreePath) {
|
|
28065
28110
|
return runGit(worktreePath, "rev-parse --abbrev-ref HEAD");
|
|
28066
28111
|
}
|
|
28112
|
+
function gitCommonDir2(cwd) {
|
|
28113
|
+
const out = runGit(cwd, "rev-parse --git-common-dir");
|
|
28114
|
+
return out ? path21.resolve(cwd, out) : void 0;
|
|
28115
|
+
}
|
|
28116
|
+
function sameGitCheckout2(a2, b2) {
|
|
28117
|
+
const left2 = gitCommonDir2(a2);
|
|
28118
|
+
const right2 = gitCommonDir2(b2);
|
|
28119
|
+
return left2 !== void 0 && right2 !== void 0 && left2 === right2;
|
|
28120
|
+
}
|
|
28121
|
+
function gitPrefix2(cwd) {
|
|
28122
|
+
const out = runGit(cwd, "rev-parse --show-prefix");
|
|
28123
|
+
return out ?? "";
|
|
28124
|
+
}
|
|
28125
|
+
function discoverSessionWorktreePath(harnessRoot, agentId) {
|
|
28126
|
+
const worktreesRoot = path21.join(os4.homedir(), "worktrees");
|
|
28127
|
+
if (!fs23.existsSync(worktreesRoot)) return void 0;
|
|
28128
|
+
const suffix = `-har-agent-${agentId}-`;
|
|
28129
|
+
const relPrefix = gitPrefix2(harnessRoot);
|
|
28130
|
+
const matches = fs23.readdirSync(worktreesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.includes(suffix)).map((entry) => path21.join(worktreesRoot, entry.name)).filter(
|
|
28131
|
+
(candidate) => sameGitCheckout2(harnessRoot, candidate) && fs23.existsSync(path21.join(candidate, relPrefix, `.env.agent.${agentId}`))
|
|
28132
|
+
);
|
|
28133
|
+
return matches.sort()[0];
|
|
28134
|
+
}
|
|
28067
28135
|
function collectWorktreeDrift(harnessRoot, worktreePath, baseCommit) {
|
|
28068
28136
|
const drift = {};
|
|
28069
28137
|
drift.detachedHead = runGit(worktreePath, "symbolic-ref -q HEAD") === void 0;
|
|
@@ -28112,12 +28180,12 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
|
|
|
28112
28180
|
"worktrees",
|
|
28113
28181
|
`${projectName}-agent-${agentId}`
|
|
28114
28182
|
);
|
|
28115
|
-
const worktreePath = session?.worktreePath && fs23.existsSync(session.worktreePath) ? session.worktreePath : fs23.existsSync(legacyWorktreePath) ? legacyWorktreePath :
|
|
28183
|
+
const worktreePath = session?.worktreePath && fs23.existsSync(session.worktreePath) ? session.worktreePath : fs23.existsSync(legacyWorktreePath) ? legacyWorktreePath : discoverSessionWorktreePath(harnessRoot, agentId);
|
|
28116
28184
|
const workDir = resolveAgentWorkDir(harnessRoot, agentId);
|
|
28117
28185
|
const envInWorkDir = workDir ? fs23.existsSync(path21.join(workDir, `.env.agent.${agentId}`)) : false;
|
|
28118
28186
|
const envInRoot = fs23.existsSync(path21.join(harnessRoot, `.env.agent.${agentId}`));
|
|
28119
28187
|
const envInWorktree = worktreePath ? fs23.existsSync(path21.join(worktreePath, `.env.agent.${agentId}`)) : false;
|
|
28120
|
-
const active = session !== void 0 && session.status
|
|
28188
|
+
const active = session !== void 0 && session.status !== "completed" || envInWorkDir || envInRoot || envInWorktree;
|
|
28121
28189
|
const latest = latestRunForAgent(runs, agentId);
|
|
28122
28190
|
const verifyRun = runs.find((r2) => r2.agentId === agentId && r2.stageId === "verify");
|
|
28123
28191
|
let previewUrls = session?.previewUrls;
|
|
@@ -28147,6 +28215,8 @@ function collectSlotStatus(harnessRoot, agentId, runs) {
|
|
|
28147
28215
|
baseCommit: session?.baseCommit,
|
|
28148
28216
|
sessionCreatedAt: session?.createdAt,
|
|
28149
28217
|
purpose: session?.purpose,
|
|
28218
|
+
sessionStatus: session?.status,
|
|
28219
|
+
lastError: session?.lastError,
|
|
28150
28220
|
...drift
|
|
28151
28221
|
};
|
|
28152
28222
|
}
|
|
@@ -28552,6 +28622,8 @@ function formatOccupiedSlot(slot) {
|
|
|
28552
28622
|
slot.worktreePath ? ` Worktree: ${slot.worktreePath}` : void 0,
|
|
28553
28623
|
slot.branch ? ` Branch: ${slot.branch}` : void 0,
|
|
28554
28624
|
slot.workDir ? ` Work dir: ${slot.workDir}` : void 0,
|
|
28625
|
+
slot.sessionStatus ? ` Status: ${slot.sessionStatus}` : void 0,
|
|
28626
|
+
slot.lastError ? ` Error: ${slot.lastError}` : void 0,
|
|
28555
28627
|
slot.sessionCreatedAt ? ` Since: ${slot.sessionCreatedAt}` : void 0,
|
|
28556
28628
|
slot.dirty ? " Git: dirty (uncommitted changes \u2014 commit or use force to discard)" : " Git: clean",
|
|
28557
28629
|
"",
|
|
@@ -29379,7 +29451,7 @@ function printNextSteps(auto2) {
|
|
|
29379
29451
|
var path28 = __toESM(require("path"));
|
|
29380
29452
|
|
|
29381
29453
|
// src/core/control-lifecycle.ts
|
|
29382
|
-
var
|
|
29454
|
+
var import_child_process4 = require("child_process");
|
|
29383
29455
|
var path27 = __toESM(require("path"));
|
|
29384
29456
|
|
|
29385
29457
|
// src/core/package-version.ts
|
|
@@ -29439,7 +29511,7 @@ function runDockerCompose(args, options) {
|
|
|
29439
29511
|
const controlDir = resolveControlDir();
|
|
29440
29512
|
const composeFiles = resolveControlComposeFiles(options);
|
|
29441
29513
|
const composeArgs = composeFiles.flatMap((file) => ["-f", file]);
|
|
29442
|
-
const result = (0,
|
|
29514
|
+
const result = (0, import_child_process4.spawnSync)("docker", ["compose", ...composeArgs, ...args], {
|
|
29443
29515
|
cwd: controlDir,
|
|
29444
29516
|
stdio: "inherit",
|
|
29445
29517
|
env: buildDockerComposeEnv()
|
|
@@ -58,6 +58,29 @@ PM2 processes for the primary application only, matching how it runs in dev.
|
|
|
58
58
|
### `.har/verify.sh`
|
|
59
59
|
Real typecheck, lint, test, and health check commands — replace all TODOs.
|
|
60
60
|
|
|
61
|
+
## Readiness vs liveness (required)
|
|
62
|
+
Do not treat a passing health check as adaptation complete. A usable agent
|
|
63
|
+
environment has layered readiness:
|
|
64
|
+
|
|
65
|
+
1. **Infra ready** — shared services and template data stores exist.
|
|
66
|
+
2. **Slot data ready** — every per-slot data store is created or cloned.
|
|
67
|
+
3. **Process ready** — primary app processes are online and health passes.
|
|
68
|
+
4. **Agent usable** — documented credentials/workflows work, required default
|
|
69
|
+
data exists, and UI/API smoke is not blocked by asset/dev-server issues.
|
|
70
|
+
|
|
71
|
+
Compare the harness against the repository's full local-dev setup. If you skip
|
|
72
|
+
heavy steps such as full seed, optional services, asset compilation modes, or
|
|
73
|
+
background daemons, add the minimum substitute in `.har/` scripts or document
|
|
74
|
+
why no substitute is needed. If the app has multiple databases/stores, provision
|
|
75
|
+
all per-slot state. If launch generates config, validate the nested keys the app
|
|
76
|
+
actually reads. Put agent-usability checks in full verify, a project-owned
|
|
77
|
+
readiness script, or documented smoke URLs. Health alone is not sufficient for
|
|
78
|
+
UI/auth apps.
|
|
79
|
+
|
|
80
|
+
Ensure `launch.sh` writes the slot registry before slow or fragile steps, and
|
|
81
|
+
`verify.sh` resolves env/work dir through `agent-slot.sh` so partial launches are
|
|
82
|
+
recoverable.
|
|
83
|
+
|
|
61
84
|
### `.har/CLAUDE.agent.md`
|
|
62
85
|
Detailed agent instructions: commands, credentials, architecture, definition of done.
|
|
63
86
|
|
|
@@ -101,4 +124,7 @@ Set slot limits in `.har/stages.json` (`agentSlots`) and `.har/harness.env` (`HA
|
|
|
101
124
|
- unused harness files deleted (`deleteHarnessFile`), e.g. `attach.sh` when unused
|
|
102
125
|
- `.har/README.md` file table matches the files that actually exist
|
|
103
126
|
- `CLAUDE.agent.md` shows only real URLs/ports and commands that run
|
|
127
|
+
- skipped full-dev setup has a minimal bootstrap or clearly documented limitation
|
|
128
|
+
- all per-slot data stores are provisioned, not only the primary database
|
|
129
|
+
- `CLAUDE.agent.md` defines what "agent usable" means, including credentials or smoke checks when applicable
|
|
104
130
|
8. **Call finishAuthoring** when complete
|
|
@@ -44,6 +44,39 @@ PM2 processes for the primary application only, matching how it runs in dev. Ski
|
|
|
44
44
|
### `.har/verify.sh`
|
|
45
45
|
Real typecheck, lint, test, and health check commands — replace all TODOs.
|
|
46
46
|
|
|
47
|
+
### Readiness vs liveness (required)
|
|
48
|
+
Do not treat a passing health check as adaptation complete. Before finishing,
|
|
49
|
+
make the harness explicit about the layers that apply to this repository:
|
|
50
|
+
|
|
51
|
+
1. **Infra ready** — shared services are running and template data stores exist.
|
|
52
|
+
2. **Slot data ready** — every per-slot data store is created or cloned, not only
|
|
53
|
+
the primary database.
|
|
54
|
+
3. **Process ready** — the primary app processes are online and
|
|
55
|
+
`HARNESS_HEALTH_CHECK_PATH` passes.
|
|
56
|
+
4. **Agent usable** — a real workflow an agent needs is possible: documented
|
|
57
|
+
credentials work, a tenant/org/project exists when the app requires one, the
|
|
58
|
+
UI is not blocked by dev-server overlays, or an authenticated API smoke works.
|
|
59
|
+
|
|
60
|
+
Compare the harness against the project's full local-dev setup. If the harness
|
|
61
|
+
intentionally skips slow or heavy steps (full seed, optional services, asset
|
|
62
|
+
mode, background daemons), add the minimum substitute directly in `.har/`
|
|
63
|
+
scripts or document why no substitute is needed. In particular:
|
|
64
|
+
|
|
65
|
+
- If `HARNESS_DB_SEED_CMD` is empty or schema-only, add an idempotent minimal
|
|
66
|
+
bootstrap step for required tenants/users/settings, or document why schema-only
|
|
67
|
+
is enough.
|
|
68
|
+
- If the app has multiple databases, schemas, queues, object stores, search
|
|
69
|
+
indexes, or other per-slot state, provision all of them in `setup-infra.sh`
|
|
70
|
+
and `launch.sh`.
|
|
71
|
+
- If launch generates config, validate the nested keys the app actually reads
|
|
72
|
+
against upstream examples/defaults, not only top-level keys.
|
|
73
|
+
- If the dev server can block agents with overlays or noisy HMR failures,
|
|
74
|
+
choose and document an agent-friendly asset mode.
|
|
75
|
+
- Put Layer 3 checks in `verify --full`, a project-owned readiness script, or
|
|
76
|
+
documented smoke URLs. Health alone is not sufficient for UI/auth apps.
|
|
77
|
+
- Ensure `launch.sh` writes the slot registry before slow or fragile steps, and
|
|
78
|
+
`verify.sh` resolves env/work dir through `agent-slot.sh`.
|
|
79
|
+
|
|
47
80
|
### Optional Playwright stage
|
|
48
81
|
If the user ran `har env add-stage playwright` (or `@playwright/test` is in package.json):
|
|
49
82
|
|
|
@@ -65,7 +98,7 @@ Agents run in parallel on configurable slot ids. Ports: `BASE + (AGENT_ID × 10)
|
|
|
65
98
|
Set slot limits in `.har/stages.json` (`agentSlots`) and `.har/harness.env` (`HARNESS_AGENT_SLOT_MIN` / `HARNESS_AGENT_SLOT_MAX`) based on machine capacity.
|
|
66
99
|
|
|
67
100
|
### Git worktree
|
|
68
|
-
`launch.sh` creates an isolated worktree at `~/worktrees/<
|
|
101
|
+
`launch.sh` creates an isolated session worktree at `~/worktrees/<base>-<sha4>-har-agent-<id>-<rand4>` by default (`HARNESS_USE_WORKTREE=true`) and records it in `.har/slots/agent-<id>.json`. Agents should commit from that worktree, not the main checkout.
|
|
69
102
|
|
|
70
103
|
## Step 3 — Update repo-root `AGENT.md`
|
|
71
104
|
|
|
@@ -112,6 +145,9 @@ The boilerplate ships more than any single repository needs. Strip it down to st
|
|
|
112
145
|
- [ ] Unused harness files are deleted (e.g. `attach.sh` when tmux isn't part of the workflow; CLI profile: `ecosystem.agent.template.cjs`, `env.template`)
|
|
113
146
|
- [ ] `.har/README.md` file table lists exactly the files that exist — no more, no less
|
|
114
147
|
- [ ] `.har/CLAUDE.agent.md` shows only real URLs/ports/credentials (e.g. drop the Frontend row for an API-only project) and project commands that actually run
|
|
148
|
+
- [ ] If full seed/setup is skipped, `.har/` provides a minimal bootstrap or clearly documents why none is needed
|
|
149
|
+
- [ ] All per-slot data stores are provisioned, not just the primary database
|
|
150
|
+
- [ ] `.har/CLAUDE.agent.md` defines what "agent usable" means for this repo: login/API smoke, credentials, required default data, and known skipped dev-setup steps
|
|
115
151
|
|
|
116
152
|
## Rules
|
|
117
153
|
|
|
@@ -28,6 +28,36 @@ Align commands and instructions with the current stack.
|
|
|
28
28
|
### `.har/env.template`, `setup-infra.sh`, `docker-compose.agent.yml`
|
|
29
29
|
Update only if infra changed.
|
|
30
30
|
|
|
31
|
+
### Readiness vs liveness regression check
|
|
32
|
+
Do not treat a passing health check as proof that the harness is still usable.
|
|
33
|
+
When maintaining an existing harness, re-check the layers that apply:
|
|
34
|
+
|
|
35
|
+
1. **Infra ready** — shared services and template data stores still match the app.
|
|
36
|
+
2. **Slot data ready** — every per-slot data store is created or cloned, not only
|
|
37
|
+
the primary database.
|
|
38
|
+
3. **Process ready** — app processes are online and `HARNESS_HEALTH_CHECK_PATH`
|
|
39
|
+
passes.
|
|
40
|
+
4. **Agent usable** — documented credentials/workflows still work, required
|
|
41
|
+
default data exists, UI/API smoke is not blocked by asset/dev-server issues,
|
|
42
|
+
and any skipped full-dev setup has a minimal substitute or clear limitation.
|
|
43
|
+
|
|
44
|
+
Look specifically for drift introduced since the last adaptation:
|
|
45
|
+
|
|
46
|
+
- A seed command was removed or made schema-only without a minimal bootstrap.
|
|
47
|
+
- A new database, schema, queue, object store, search index, or other per-slot
|
|
48
|
+
dependency was added but launch only provisions the original primary store.
|
|
49
|
+
- Config generation writes plausible top-level keys while the app reads nested
|
|
50
|
+
defaults from another file.
|
|
51
|
+
- The dev server mode is fine for humans but blocks browser automation or agents
|
|
52
|
+
with overlays/noisy HMR.
|
|
53
|
+
- `verify.sh` became health-only and no longer checks the key workflow that makes
|
|
54
|
+
the slot usable.
|
|
55
|
+
- `launch.sh` writes the slot registry only after fragile late steps; partial
|
|
56
|
+
launches must remain discoverable by verify/status/teardown.
|
|
57
|
+
|
|
58
|
+
Update `.har/CLAUDE.agent.md` with skipped setup steps, substitutes, credentials,
|
|
59
|
+
and the repo-specific definition of "agent usable."
|
|
60
|
+
|
|
31
61
|
### HAR platform upgrades checklist
|
|
32
62
|
|
|
33
63
|
When upgrading `@osfactory/har` or adopting new harness standards:
|
|
@@ -22,9 +22,23 @@ This slot runs **only the primary application** (`HARNESS_PRIMARY_APP`). Externa
|
|
|
22
22
|
./.har/agent-cli.sh ${AGENT_ID} health
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Readiness
|
|
26
|
+
|
|
27
|
+
Adapt this section for the repository. A passing health check means the process
|
|
28
|
+
is alive; it does not automatically mean an agent can use the app.
|
|
29
|
+
|
|
30
|
+
- **Health**: `./.har/agent-cli.sh ${AGENT_ID} health`
|
|
31
|
+
- **Agent-usable smoke**: document the login/API/UI workflow agents should try,
|
|
32
|
+
or wire it into `HARNESS_READINESS_CMD` / full verify.
|
|
33
|
+
- **Credentials/default data**: document any test users, tenants, projects, or
|
|
34
|
+
settings created by the harness.
|
|
35
|
+
- **Skipped full-dev setup**: document anything intentionally omitted from the
|
|
36
|
+
upstream developer setup and the minimal substitute in `.har/`.
|
|
37
|
+
|
|
25
38
|
## Definition of done
|
|
26
39
|
|
|
27
40
|
- [ ] Full verification returns `"status": "pass"` (`har env verify ${AGENT_ID} --full`, MCP `har_run_verification` with `full: true`, or `./.har/verify.sh ${AGENT_ID} --full`)
|
|
41
|
+
- [ ] The slot is agent-usable for this repo's documented smoke workflow, not only health-check green
|
|
28
42
|
- [ ] When `stages/browser-e2e.sh` exists, full verify includes Playwright — adapt specs under `tests/` for UI changes
|
|
29
43
|
- [ ] New behavior has automated test coverage (unit and/or browser as appropriate)
|
|
30
44
|
- [ ] Changes committed **in the session worktree** with a clear message
|
|
@@ -61,10 +61,27 @@ Read **`stages.json`** for registered stages and **`verificationStages`** for th
|
|
|
61
61
|
| Mode | Command | Typical steps |
|
|
62
62
|
|------|---------|---------------|
|
|
63
63
|
| Quick | `har env verify <id>` or `verify.sh <id>` | Project checks in `verify.sh` (stops early on failure) |
|
|
64
|
-
| Full | `har env verify <id> --full` or `verify.sh <id> --full` | Quick steps + lint + **`browser-e2e`** when `.har/stages/browser-e2e.sh` exists |
|
|
64
|
+
| Full | `har env verify <id> --full` or `verify.sh <id> --full` | Quick steps + lint + optional readiness smoke + **`browser-e2e`** when `.har/stages/browser-e2e.sh` exists |
|
|
65
65
|
|
|
66
66
|
Install Playwright stage: `har env add-stage playwright` (optional). UI changes should add or update specs under `tests/`.
|
|
67
67
|
|
|
68
|
+
## Readiness layers
|
|
69
|
+
|
|
70
|
+
Do not treat a health endpoint as the whole definition of success. Adapt this
|
|
71
|
+
section for the repository:
|
|
72
|
+
|
|
73
|
+
| Layer | What it means | Where to encode it |
|
|
74
|
+
|-------|---------------|--------------------|
|
|
75
|
+
| Infra ready | Shared services and template data stores exist | `setup-infra.sh`, `docker-compose.agent.yml` |
|
|
76
|
+
| Slot data ready | Every per-slot data store is created/cloned | `launch.sh` |
|
|
77
|
+
| Process ready | Primary app is running and health passes | `launch.sh`, `verify.sh` |
|
|
78
|
+
| Agent usable | Login/API/UI smoke works with documented data | `HARNESS_READINESS_CMD`, browser-e2e, `CLAUDE.agent.md` |
|
|
79
|
+
|
|
80
|
+
If full local-dev setup is too heavy to run in the harness, document the skipped
|
|
81
|
+
steps and add the minimum substitute directly in `.har/` scripts (for example an
|
|
82
|
+
idempotent bootstrap for required users/tenants/settings). Health alone is not
|
|
83
|
+
enough for UI/auth apps.
|
|
84
|
+
|
|
68
85
|
## Run history
|
|
69
86
|
|
|
70
87
|
| Entry point | Writes `.har/runs/`? |
|
|
@@ -56,7 +56,13 @@ process.stdin.on('end', () => {
|
|
|
56
56
|
fi
|
|
57
57
|
|
|
58
58
|
if [ "$PM2_FOUND" = true ]; then
|
|
59
|
-
|
|
59
|
+
REG_FILE="$(slot_registry_file "$AGENT_ID")"
|
|
60
|
+
if [ ! -f "$REG_FILE" ]; then
|
|
61
|
+
echo "Warning: slot registry missing at $REG_FILE — verify/teardown may not resolve this slot."
|
|
62
|
+
fi
|
|
63
|
+
if [ -z "$ENV_FILE" ]; then
|
|
64
|
+
echo "Warning: .env.agent.${AGENT_ID} could not be resolved — relaunch with --replace when ready."
|
|
65
|
+
fi
|
|
60
66
|
elif [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then
|
|
61
67
|
echo "Agent ${AGENT_ID}: active (no PM2 processes)"
|
|
62
68
|
# shellcheck source=/dev/null
|
|
@@ -50,7 +50,8 @@ try {
|
|
|
50
50
|
# Writes the registry entry from SLOT_* variables:
|
|
51
51
|
# required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
|
|
52
52
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
53
|
-
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
53
|
+
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
54
|
+
# SLOT_PURPOSE, SLOT_STATUS, SLOT_LAST_ERROR
|
|
54
55
|
write_slot_registry() {
|
|
55
56
|
local file
|
|
56
57
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -65,7 +66,7 @@ const entry = {
|
|
|
65
66
|
mode: e.SLOT_MODE,
|
|
66
67
|
workDir: e.SLOT_WORK_DIR,
|
|
67
68
|
createdAt: new Date().toISOString(),
|
|
68
|
-
status: "active",
|
|
69
|
+
status: e.SLOT_STATUS || "active",
|
|
69
70
|
};
|
|
70
71
|
if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
|
|
71
72
|
if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
|
|
@@ -73,6 +74,7 @@ if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
|
|
|
73
74
|
if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
|
|
74
75
|
if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
|
|
75
76
|
if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
|
|
77
|
+
if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
|
|
76
78
|
for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
|
|
77
79
|
if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
|
|
78
80
|
}
|
|
@@ -115,13 +117,15 @@ slot_dirty_summary() {
|
|
|
115
117
|
# Print a warning before replacing an occupied slot (stdout — visible to agents).
|
|
116
118
|
print_slot_replace_warning() {
|
|
117
119
|
local agent_id="$1"
|
|
118
|
-
local reg wt branch work_dir purpose created dirty_summary head
|
|
120
|
+
local reg wt branch work_dir purpose created status last_error dirty_summary head
|
|
119
121
|
reg="$(slot_registry_file "$agent_id")"
|
|
120
122
|
wt="$(existing_slot_worktree "$agent_id")"
|
|
121
123
|
branch="$(read_slot_field "$reg" branch || true)"
|
|
122
124
|
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
123
125
|
purpose="$(read_slot_field "$reg" purpose || true)"
|
|
124
126
|
created="$(read_slot_field "$reg" createdAt || true)"
|
|
127
|
+
status="$(read_slot_field "$reg" status || true)"
|
|
128
|
+
last_error="$(read_slot_field "$reg" lastError || true)"
|
|
125
129
|
dirty_summary="$(slot_dirty_summary "$wt")"
|
|
126
130
|
if [ -n "$wt" ] && [ -d "$wt" ]; then
|
|
127
131
|
head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
|
|
@@ -133,6 +137,8 @@ print_slot_replace_warning() {
|
|
|
133
137
|
[ -n "$wt" ] && echo " Worktree: ${wt}" >&2
|
|
134
138
|
[ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
|
|
135
139
|
[ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
|
|
140
|
+
[ -n "$status" ] && echo " Status: ${status}" >&2
|
|
141
|
+
[ -n "$last_error" ] && echo " Error: ${last_error}" >&2
|
|
136
142
|
[ -n "$created" ] && echo " Since: ${created}" >&2
|
|
137
143
|
echo " Git: ${dirty_summary}" >&2
|
|
138
144
|
echo "" >&2
|
|
@@ -182,11 +188,30 @@ require_slot_replace_confirm() {
|
|
|
182
188
|
exit 2
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
git_common_dir() {
|
|
192
|
+
local cwd="$1"
|
|
193
|
+
local out
|
|
194
|
+
out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
195
|
+
case "$out" in
|
|
196
|
+
/*) echo "$out" ;;
|
|
197
|
+
*) (cd "$cwd" && cd "$out" && pwd) ;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
same_git_checkout() {
|
|
202
|
+
local left right
|
|
203
|
+
left="$(git_common_dir "$1" || true)"
|
|
204
|
+
right="$(git_common_dir "$2" || true)"
|
|
205
|
+
[ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
# Echo the previous session's worktree path for a slot: registry first, then
|
|
186
|
-
#
|
|
209
|
+
# legacy and randomized session worktree fallbacks. Empty output when none exists.
|
|
187
210
|
existing_slot_worktree() {
|
|
188
211
|
local agent_id="$1"
|
|
189
|
-
local reg path
|
|
212
|
+
local reg path candidate repo_root rel_prefix
|
|
213
|
+
repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
214
|
+
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
190
215
|
reg="$(slot_registry_file "$agent_id")"
|
|
191
216
|
if [ -f "$reg" ]; then
|
|
192
217
|
path="$(read_slot_field "$reg" worktreePath || true)"
|
|
@@ -198,12 +223,20 @@ existing_slot_worktree() {
|
|
|
198
223
|
path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
|
|
199
224
|
if [ -d "$path" ]; then
|
|
200
225
|
echo "$path"
|
|
226
|
+
return 0
|
|
201
227
|
fi
|
|
228
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
|
|
229
|
+
[ -d "$candidate" ] || continue
|
|
230
|
+
same_git_checkout "$repo_root" "$candidate" || continue
|
|
231
|
+
[ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
|
|
232
|
+
echo "$candidate"
|
|
233
|
+
return 0
|
|
234
|
+
done
|
|
202
235
|
return 0
|
|
203
236
|
}
|
|
204
237
|
|
|
205
|
-
# Resolve .env.agent.<id> — registry work dir first, then legacy
|
|
206
|
-
#
|
|
238
|
+
# Resolve .env.agent.<id> — registry work dir first, then legacy and
|
|
239
|
+
# randomized session worktree fallbacks.
|
|
207
240
|
resolve_agent_env_file() {
|
|
208
241
|
local agent_id="$1"
|
|
209
242
|
local repo_root="$2"
|
|
@@ -229,6 +262,15 @@ resolve_agent_env_file() {
|
|
|
229
262
|
return 0
|
|
230
263
|
fi
|
|
231
264
|
done
|
|
265
|
+
local candidate_dir
|
|
266
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
|
|
267
|
+
if [ -f "$candidate" ]; then
|
|
268
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
269
|
+
same_git_checkout "$repo_root" "$candidate_dir" || continue
|
|
270
|
+
echo "$candidate"
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
232
274
|
return 1
|
|
233
275
|
}
|
|
234
276
|
|
|
@@ -278,3 +320,14 @@ run_browser_e2e_if_present() {
|
|
|
278
320
|
"$e2e" "$agent_id"
|
|
279
321
|
fi
|
|
280
322
|
}
|
|
323
|
+
|
|
324
|
+
# Optional project-owned "agent usable" smoke beyond health.
|
|
325
|
+
run_readiness_if_configured() {
|
|
326
|
+
local agent_id="$1"
|
|
327
|
+
if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
|
|
328
|
+
echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
|
|
329
|
+
return 0
|
|
330
|
+
fi
|
|
331
|
+
local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
|
|
332
|
+
eval "$cmd"
|
|
333
|
+
}
|
|
@@ -48,3 +48,18 @@ har_pg() {
|
|
|
48
48
|
# Database setup commands (run once against template DB in setup-infra.sh)
|
|
49
49
|
export HARNESS_DB_MIGRATE_CMD="echo 'TODO: set migrate command'"
|
|
50
50
|
export HARNESS_DB_SEED_CMD="echo 'TODO: set seed command'"
|
|
51
|
+
|
|
52
|
+
# Optional readiness/data hooks for larger apps. Enable only when this project
|
|
53
|
+
# needs them, and keep the scripts idempotent.
|
|
54
|
+
#
|
|
55
|
+
# Multiple per-slot data stores can be represented as logical_name:template_db
|
|
56
|
+
# pairs and handled directly in setup-infra.sh / launch.sh.
|
|
57
|
+
# export HARNESS_TEMPLATE_DBS="main:template___PROJECT_NAME__"
|
|
58
|
+
#
|
|
59
|
+
# Use a minimal bootstrap when full seed is too slow/heavy but the app still
|
|
60
|
+
# needs default tenants, users, settings, feature flags, or similar data.
|
|
61
|
+
# export HARNESS_DB_MINIMAL_BOOTSTRAP_CMD="./.har/bootstrap-data.sh"
|
|
62
|
+
#
|
|
63
|
+
# Use readiness checks for "agent usable" smoke beyond a health endpoint:
|
|
64
|
+
# login/API auth, representative UI page, required data, secondary stores.
|
|
65
|
+
# export HARNESS_READINESS_CMD="./.har/readiness.sh"
|
|
@@ -147,6 +147,55 @@ REPO_ROOT="$WORK_DIR" \
|
|
|
147
147
|
envsubst '${AGENT_ID} ${API_PORT} ${FE_PORT} ${DEBUG_PORT} ${DB_PORT} ${MINIO_PORT} ${BROWSER_PORT} ${REPO_ROOT}' \
|
|
148
148
|
< "$SCRIPT_DIR/env.template" > "$ENV_FILE"
|
|
149
149
|
|
|
150
|
+
REGISTRY_WRITTEN=false
|
|
151
|
+
mark_slot_failed() {
|
|
152
|
+
local exit_code="$?"
|
|
153
|
+
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
154
|
+
log "Launch failed after creating the session. Recording failed slot state..."
|
|
155
|
+
set +e
|
|
156
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
157
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
158
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
159
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
160
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
161
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
162
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
163
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
164
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
165
|
+
SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
|
|
166
|
+
SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
|
|
167
|
+
SLOT_STATUS="failed" \
|
|
168
|
+
SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
|
|
169
|
+
write_slot_registry
|
|
170
|
+
log " Work dir: ${WORK_DIR}"
|
|
171
|
+
log " Env file: ${ENV_FILE}"
|
|
172
|
+
log " Recovery: fix the failure, then relaunch with --replace when ready."
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
trap mark_slot_failed EXIT
|
|
176
|
+
|
|
177
|
+
# Record the session before slow or fragile setup so verify/status/teardown can
|
|
178
|
+
# recover partial launches that already created a worktree and env file.
|
|
179
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
180
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
181
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
182
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
183
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
184
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
185
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
186
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
187
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
188
|
+
SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
|
|
189
|
+
SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
|
|
190
|
+
SLOT_STATUS="starting" \
|
|
191
|
+
write_slot_registry
|
|
192
|
+
REGISTRY_WRITTEN=true
|
|
193
|
+
|
|
194
|
+
if [ -n "${HARNESS_DB_MINIMAL_BOOTSTRAP_CMD:-}" ]; then
|
|
195
|
+
log "Running minimal data bootstrap..."
|
|
196
|
+
(cd "$WORK_DIR" && set -a && . "$ENV_FILE" && set +a && eval "$HARNESS_DB_MINIMAL_BOOTSTRAP_CMD")
|
|
197
|
+
fi
|
|
198
|
+
|
|
150
199
|
# Generate PM2 ecosystem config
|
|
151
200
|
ECOSYSTEM_FILE="$WORK_DIR/ecosystem.agent.${AGENT_ID}.config.cjs"
|
|
152
201
|
log "Generating $ECOSYSTEM_FILE..."
|
|
@@ -199,6 +248,7 @@ SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
|
199
248
|
SLOT_PURPOSE="${PURPOSE}" \
|
|
200
249
|
SLOT_PORTS_JSON="{\"frontend\":${FE_PORT},\"api\":${API_PORT},\"debug\":${DEBUG_PORT}}" \
|
|
201
250
|
SLOT_PREVIEW_URLS_JSON="{\"frontend\":\"http://localhost:${FE_PORT}\",\"api\":\"http://localhost:${API_PORT}\"}" \
|
|
251
|
+
SLOT_STATUS="active" \
|
|
202
252
|
write_slot_registry
|
|
203
253
|
|
|
204
254
|
# Launch Claude Code in tmux (optional)
|
|
@@ -38,6 +38,14 @@ WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
|
|
|
38
38
|
API_PORT="${API_PORT:-$(( HARNESS_API_BASE_PORT + AGENT_ID * 10 ))}"
|
|
39
39
|
|
|
40
40
|
echo "==> Verifying agent ${AGENT_ID} (work dir: ${WORK_DIR})..." >&2
|
|
41
|
+
REG_FILE="$(slot_registry_file "$AGENT_ID")"
|
|
42
|
+
echo " Work dir: ${WORK_DIR}" >&2
|
|
43
|
+
echo " Env file: ${ENV_FILE}" >&2
|
|
44
|
+
if [ -f "$REG_FILE" ]; then
|
|
45
|
+
echo " Registry: ${REG_FILE}" >&2
|
|
46
|
+
else
|
|
47
|
+
echo " Registry: missing (${REG_FILE})" >&2
|
|
48
|
+
fi
|
|
41
49
|
|
|
42
50
|
OVERALL_PASS=true
|
|
43
51
|
START_TOTAL=$(now_ms)
|
|
@@ -134,6 +142,7 @@ run_http_step "api-health" "http://localhost:${API_PORT}${HARNESS_HEALTH_CHECK_P
|
|
|
134
142
|
|
|
135
143
|
if [ -n "$FULL" ]; then
|
|
136
144
|
run_step "lint" "echo 'TODO: npm run lint'" || true
|
|
145
|
+
run_step "readiness" "run_readiness_if_configured \"$AGENT_ID\"" || true
|
|
137
146
|
run_step "browser-e2e" "run_browser_e2e_if_present \"$SCRIPT_DIR\" \"$AGENT_ID\"" || true
|
|
138
147
|
fi
|
|
139
148
|
|
|
@@ -15,9 +15,17 @@
|
|
|
15
15
|
./.har/agent-cli.sh ${AGENT_ID} status
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Readiness
|
|
19
|
+
|
|
20
|
+
Adapt this section for the repository. For pure CLI/library repos, full verify
|
|
21
|
+
may be enough. If the project needs services, auth, seeded data, or a sample
|
|
22
|
+
workflow, document the required credentials/default data and wire a smoke into
|
|
23
|
+
`HARNESS_READINESS_CMD` or full verify.
|
|
24
|
+
|
|
18
25
|
## Definition of done
|
|
19
26
|
|
|
20
27
|
- [ ] Full verification returns `"status": "pass"` (`har env verify ${AGENT_ID} --full`, MCP `har_run_verification` with `full: true`, or `./.har/verify.sh ${AGENT_ID} --full`)
|
|
28
|
+
- [ ] The slot is agent-usable for this repo's documented smoke workflow when runtime services are involved
|
|
21
29
|
- [ ] When `stages/browser-e2e.sh` exists, full verify includes Playwright — adapt specs under `tests/` for UI changes
|
|
22
30
|
- [ ] New behavior has automated test coverage
|
|
23
31
|
- [ ] Changes committed **in the session worktree** with a clear message
|
|
@@ -59,7 +59,12 @@ Read **`stages.json`** and **`verificationStages`**. Optional: `har env add-stag
|
|
|
59
59
|
| Mode | Command | Typical steps |
|
|
60
60
|
|------|---------|---------------|
|
|
61
61
|
| Quick | `har env verify <id>` or `verify.sh <id>` | typecheck, unit tests |
|
|
62
|
-
| Full | `har env verify <id> --full` or `verify.sh <id> --full` | + lint,
|
|
62
|
+
| Full | `har env verify <id> --full` or `verify.sh <id> --full` | + lint, optional readiness smoke, **browser-e2e** when `stages/browser-e2e.sh` exists |
|
|
63
|
+
|
|
64
|
+
For repos that need runtime services, distinguish health from usability. If the
|
|
65
|
+
harness skips slow local-dev setup, document the skipped steps and add a minimal
|
|
66
|
+
bootstrap/readiness check when agents need default data, credentials, or an
|
|
67
|
+
authenticated workflow.
|
|
63
68
|
|
|
64
69
|
Use `har env launch 1 --no-worktree` or `./.har/launch.sh 1 --no-worktree` only when working in the repo root.
|
|
65
70
|
|
|
@@ -50,7 +50,8 @@ try {
|
|
|
50
50
|
# Writes the registry entry from SLOT_* variables:
|
|
51
51
|
# required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
|
|
52
52
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
53
|
-
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
53
|
+
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
54
|
+
# SLOT_PURPOSE, SLOT_STATUS, SLOT_LAST_ERROR
|
|
54
55
|
write_slot_registry() {
|
|
55
56
|
local file
|
|
56
57
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -65,7 +66,7 @@ const entry = {
|
|
|
65
66
|
mode: e.SLOT_MODE,
|
|
66
67
|
workDir: e.SLOT_WORK_DIR,
|
|
67
68
|
createdAt: new Date().toISOString(),
|
|
68
|
-
status: "active",
|
|
69
|
+
status: e.SLOT_STATUS || "active",
|
|
69
70
|
};
|
|
70
71
|
if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
|
|
71
72
|
if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
|
|
@@ -73,6 +74,7 @@ if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
|
|
|
73
74
|
if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
|
|
74
75
|
if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
|
|
75
76
|
if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
|
|
77
|
+
if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
|
|
76
78
|
for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
|
|
77
79
|
if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
|
|
78
80
|
}
|
|
@@ -115,13 +117,15 @@ slot_dirty_summary() {
|
|
|
115
117
|
# Print a warning before replacing an occupied slot (stdout — visible to agents).
|
|
116
118
|
print_slot_replace_warning() {
|
|
117
119
|
local agent_id="$1"
|
|
118
|
-
local reg wt branch work_dir purpose created dirty_summary head
|
|
120
|
+
local reg wt branch work_dir purpose created status last_error dirty_summary head
|
|
119
121
|
reg="$(slot_registry_file "$agent_id")"
|
|
120
122
|
wt="$(existing_slot_worktree "$agent_id")"
|
|
121
123
|
branch="$(read_slot_field "$reg" branch || true)"
|
|
122
124
|
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
123
125
|
purpose="$(read_slot_field "$reg" purpose || true)"
|
|
124
126
|
created="$(read_slot_field "$reg" createdAt || true)"
|
|
127
|
+
status="$(read_slot_field "$reg" status || true)"
|
|
128
|
+
last_error="$(read_slot_field "$reg" lastError || true)"
|
|
125
129
|
dirty_summary="$(slot_dirty_summary "$wt")"
|
|
126
130
|
if [ -n "$wt" ] && [ -d "$wt" ]; then
|
|
127
131
|
head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
|
|
@@ -133,6 +137,8 @@ print_slot_replace_warning() {
|
|
|
133
137
|
[ -n "$wt" ] && echo " Worktree: ${wt}" >&2
|
|
134
138
|
[ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
|
|
135
139
|
[ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
|
|
140
|
+
[ -n "$status" ] && echo " Status: ${status}" >&2
|
|
141
|
+
[ -n "$last_error" ] && echo " Error: ${last_error}" >&2
|
|
136
142
|
[ -n "$created" ] && echo " Since: ${created}" >&2
|
|
137
143
|
echo " Git: ${dirty_summary}" >&2
|
|
138
144
|
echo "" >&2
|
|
@@ -182,11 +188,30 @@ require_slot_replace_confirm() {
|
|
|
182
188
|
exit 2
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
git_common_dir() {
|
|
192
|
+
local cwd="$1"
|
|
193
|
+
local out
|
|
194
|
+
out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
195
|
+
case "$out" in
|
|
196
|
+
/*) echo "$out" ;;
|
|
197
|
+
*) (cd "$cwd" && cd "$out" && pwd) ;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
same_git_checkout() {
|
|
202
|
+
local left right
|
|
203
|
+
left="$(git_common_dir "$1" || true)"
|
|
204
|
+
right="$(git_common_dir "$2" || true)"
|
|
205
|
+
[ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
# Echo the previous session's worktree path for a slot: registry first, then
|
|
186
|
-
#
|
|
209
|
+
# legacy and randomized session worktree fallbacks. Empty output when none exists.
|
|
187
210
|
existing_slot_worktree() {
|
|
188
211
|
local agent_id="$1"
|
|
189
|
-
local reg path
|
|
212
|
+
local reg path candidate repo_root rel_prefix
|
|
213
|
+
repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
214
|
+
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
190
215
|
reg="$(slot_registry_file "$agent_id")"
|
|
191
216
|
if [ -f "$reg" ]; then
|
|
192
217
|
path="$(read_slot_field "$reg" worktreePath || true)"
|
|
@@ -198,12 +223,20 @@ existing_slot_worktree() {
|
|
|
198
223
|
path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
|
|
199
224
|
if [ -d "$path" ]; then
|
|
200
225
|
echo "$path"
|
|
226
|
+
return 0
|
|
201
227
|
fi
|
|
228
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
|
|
229
|
+
[ -d "$candidate" ] || continue
|
|
230
|
+
same_git_checkout "$repo_root" "$candidate" || continue
|
|
231
|
+
[ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
|
|
232
|
+
echo "$candidate"
|
|
233
|
+
return 0
|
|
234
|
+
done
|
|
202
235
|
return 0
|
|
203
236
|
}
|
|
204
237
|
|
|
205
|
-
# Resolve .env.agent.<id> — registry work dir first, then legacy
|
|
206
|
-
#
|
|
238
|
+
# Resolve .env.agent.<id> — registry work dir first, then legacy and
|
|
239
|
+
# randomized session worktree fallbacks.
|
|
207
240
|
resolve_agent_env_file() {
|
|
208
241
|
local agent_id="$1"
|
|
209
242
|
local repo_root="$2"
|
|
@@ -229,6 +262,15 @@ resolve_agent_env_file() {
|
|
|
229
262
|
return 0
|
|
230
263
|
fi
|
|
231
264
|
done
|
|
265
|
+
local candidate_dir
|
|
266
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
|
|
267
|
+
if [ -f "$candidate" ]; then
|
|
268
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
269
|
+
same_git_checkout "$repo_root" "$candidate_dir" || continue
|
|
270
|
+
echo "$candidate"
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
232
274
|
return 1
|
|
233
275
|
}
|
|
234
276
|
|
|
@@ -278,3 +320,14 @@ run_browser_e2e_if_present() {
|
|
|
278
320
|
"$e2e" "$agent_id"
|
|
279
321
|
fi
|
|
280
322
|
}
|
|
323
|
+
|
|
324
|
+
# Optional project-owned "agent usable" smoke beyond health.
|
|
325
|
+
run_readiness_if_configured() {
|
|
326
|
+
local agent_id="$1"
|
|
327
|
+
if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
|
|
328
|
+
echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
|
|
329
|
+
return 0
|
|
330
|
+
fi
|
|
331
|
+
local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
|
|
332
|
+
eval "$cmd"
|
|
333
|
+
}
|
|
@@ -38,3 +38,11 @@ har_pg() {
|
|
|
38
38
|
|
|
39
39
|
export HARNESS_DB_MIGRATE_CMD="true"
|
|
40
40
|
export HARNESS_DB_SEED_CMD="true"
|
|
41
|
+
|
|
42
|
+
# Optional readiness/data hooks for repositories that need more than static
|
|
43
|
+
# CLI/library checks. Enable only when this project needs them, and keep scripts
|
|
44
|
+
# idempotent.
|
|
45
|
+
#
|
|
46
|
+
# export HARNESS_TEMPLATE_DBS="main:template___PROJECT_NAME__"
|
|
47
|
+
# export HARNESS_DB_MINIMAL_BOOTSTRAP_CMD="./.har/bootstrap-data.sh"
|
|
48
|
+
# export HARNESS_READINESS_CMD="./.har/readiness.sh"
|
|
@@ -94,6 +94,46 @@ WORKTREE_DIR=${WORKTREE_DIR}
|
|
|
94
94
|
NODE_ENV=test
|
|
95
95
|
EOF
|
|
96
96
|
|
|
97
|
+
REGISTRY_WRITTEN=false
|
|
98
|
+
mark_slot_failed() {
|
|
99
|
+
local exit_code="$?"
|
|
100
|
+
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
101
|
+
log "Launch failed after creating the session. Recording failed slot state..."
|
|
102
|
+
set +e
|
|
103
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
104
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
105
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
106
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
107
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
108
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
109
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
110
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
111
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
112
|
+
SLOT_STATUS="failed" \
|
|
113
|
+
SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
|
|
114
|
+
write_slot_registry
|
|
115
|
+
log " Work dir: ${WORK_DIR}"
|
|
116
|
+
log " Env file: ${ENV_FILE}"
|
|
117
|
+
log " Recovery: fix the failure, then relaunch with --replace when ready."
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
trap mark_slot_failed EXIT
|
|
121
|
+
|
|
122
|
+
# Record the session before slow or fragile setup so verify/status/teardown can
|
|
123
|
+
# recover partial launches that already created a worktree and env file.
|
|
124
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
125
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
126
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
127
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
128
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
129
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
130
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
131
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
132
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
133
|
+
SLOT_STATUS="starting" \
|
|
134
|
+
write_slot_registry
|
|
135
|
+
REGISTRY_WRITTEN=true
|
|
136
|
+
|
|
97
137
|
if [ -f "$WORK_DIR/package.json" ]; then
|
|
98
138
|
log "Installing dependencies..."
|
|
99
139
|
(cd "$WORK_DIR" && npm install --silent)
|
|
@@ -118,6 +158,7 @@ SLOT_BRANCH="${BRANCH:-}" \
|
|
|
118
158
|
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
119
159
|
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
120
160
|
SLOT_PURPOSE="${PURPOSE}" \
|
|
161
|
+
SLOT_STATUS="active" \
|
|
121
162
|
write_slot_registry
|
|
122
163
|
|
|
123
164
|
log "Agent $AGENT_ID is ready."
|
|
@@ -35,6 +35,14 @@ set +a
|
|
|
35
35
|
WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
|
|
36
36
|
|
|
37
37
|
echo "==> Verifying agent ${AGENT_ID} in ${WORK_DIR}..." >&2
|
|
38
|
+
REG_FILE="$(slot_registry_file "$AGENT_ID")"
|
|
39
|
+
echo " Work dir: ${WORK_DIR}" >&2
|
|
40
|
+
echo " Env file: ${ENV_FILE}" >&2
|
|
41
|
+
if [ -f "$REG_FILE" ]; then
|
|
42
|
+
echo " Registry: ${REG_FILE}" >&2
|
|
43
|
+
else
|
|
44
|
+
echo " Registry: missing (${REG_FILE})" >&2
|
|
45
|
+
fi
|
|
38
46
|
|
|
39
47
|
OVERALL_PASS=true
|
|
40
48
|
START_TOTAL=$(now_ms)
|
|
@@ -88,6 +96,7 @@ run_step "unit-tests" "npm test" || { [ -z "$FULL" ] && true; }
|
|
|
88
96
|
|
|
89
97
|
if [ -n "$FULL" ]; then
|
|
90
98
|
run_step "lint" "npm run lint" || true
|
|
99
|
+
run_step "readiness" "run_readiness_if_configured \"$AGENT_ID\"" || true
|
|
91
100
|
run_step "browser-e2e" "run_browser_e2e_if_present \"$SCRIPT_DIR\" \"$AGENT_ID\"" || true
|
|
92
101
|
fi
|
|
93
102
|
|
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
./.har/agent-cli.sh ${AGENT_ID} status
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## Readiness
|
|
21
|
+
|
|
22
|
+
Adapt this section for the repository. A successful build/test run does not
|
|
23
|
+
always mean an agent can use the app. If the app needs a backend, credentials,
|
|
24
|
+
seeded data, or a simulator flow, document it here and wire the smoke into
|
|
25
|
+
`HARNESS_READINESS_CMD`, RocketSim flows, or full verify.
|
|
26
|
+
|
|
20
27
|
## Definition of done
|
|
21
28
|
|
|
22
29
|
- [ ] Full verification returns `"status": "pass"` (`har env verify ${AGENT_ID} --full`, MCP `har_run_verification` with `full: true`, or `./.har/verify.sh ${AGENT_ID} --full`)
|
|
30
|
+
- [ ] The slot is agent-usable for this repo's documented smoke workflow when runtime services are involved
|
|
23
31
|
- [ ] When `stages/rocketsim-flows.sh` exists, full verify includes user-flow validation — add or update flow scripts in `flows/` for UI changes
|
|
24
32
|
- [ ] New behavior has automated test coverage (unit tests via XCTest)
|
|
25
33
|
- [ ] Changes committed **in the session worktree** with a clear message
|
|
@@ -67,7 +67,11 @@ This installs `.har/stages/rocketsim-flows.sh` and a `flows/` directory. Add flo
|
|
|
67
67
|
| Mode | Command | Typical steps |
|
|
68
68
|
|------|---------|---------------|
|
|
69
69
|
| Quick | `har env verify <id>` | build, unit-tests |
|
|
70
|
-
| Full | `har env verify <id> --full` | + lint, **rocketsim-flows** when installed |
|
|
70
|
+
| Full | `har env verify <id> --full` | + lint, optional readiness smoke, **rocketsim-flows** when installed |
|
|
71
|
+
|
|
72
|
+
For apps that depend on local backends, auth, seeded state, or simulator flows,
|
|
73
|
+
distinguish build/test health from agent usability. Document any skipped full
|
|
74
|
+
dev setup and add a readiness command when agents need a real workflow to pass.
|
|
71
75
|
|
|
72
76
|
## Configuration
|
|
73
77
|
|
|
@@ -50,7 +50,8 @@ try {
|
|
|
50
50
|
# Writes the registry entry from SLOT_* variables:
|
|
51
51
|
# required: SLOT_AGENT_ID, SLOT_MODE (worktree|root), SLOT_WORK_DIR
|
|
52
52
|
# optional: SLOT_SUFFIX, SLOT_WORKTREE_PATH, SLOT_BRANCH, SLOT_BASE_BRANCH,
|
|
53
|
-
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
53
|
+
# SLOT_BASE_COMMIT, SLOT_PORTS_JSON, SLOT_PREVIEW_URLS_JSON,
|
|
54
|
+
# SLOT_PURPOSE, SLOT_STATUS, SLOT_LAST_ERROR
|
|
54
55
|
write_slot_registry() {
|
|
55
56
|
local file
|
|
56
57
|
file="$(slot_registry_file "$SLOT_AGENT_ID")"
|
|
@@ -65,7 +66,7 @@ const entry = {
|
|
|
65
66
|
mode: e.SLOT_MODE,
|
|
66
67
|
workDir: e.SLOT_WORK_DIR,
|
|
67
68
|
createdAt: new Date().toISOString(),
|
|
68
|
-
status: "active",
|
|
69
|
+
status: e.SLOT_STATUS || "active",
|
|
69
70
|
};
|
|
70
71
|
if (e.SLOT_SUFFIX) entry.suffix = e.SLOT_SUFFIX;
|
|
71
72
|
if (e.SLOT_WORKTREE_PATH) entry.worktreePath = e.SLOT_WORKTREE_PATH;
|
|
@@ -73,6 +74,7 @@ if (e.SLOT_BRANCH) entry.branch = e.SLOT_BRANCH;
|
|
|
73
74
|
if (e.SLOT_BASE_BRANCH) entry.baseBranch = e.SLOT_BASE_BRANCH;
|
|
74
75
|
if (e.SLOT_BASE_COMMIT) entry.baseCommit = e.SLOT_BASE_COMMIT;
|
|
75
76
|
if (e.SLOT_PURPOSE) entry.purpose = e.SLOT_PURPOSE;
|
|
77
|
+
if (e.SLOT_LAST_ERROR) entry.lastError = e.SLOT_LAST_ERROR;
|
|
76
78
|
for (const [key, env] of [["ports", "SLOT_PORTS_JSON"], ["previewUrls", "SLOT_PREVIEW_URLS_JSON"]]) {
|
|
77
79
|
if (e[env]) try { entry[key] = JSON.parse(e[env]); } catch {}
|
|
78
80
|
}
|
|
@@ -115,13 +117,15 @@ slot_dirty_summary() {
|
|
|
115
117
|
# Print a warning before replacing an occupied slot (stdout — visible to agents).
|
|
116
118
|
print_slot_replace_warning() {
|
|
117
119
|
local agent_id="$1"
|
|
118
|
-
local reg wt branch work_dir purpose created dirty_summary head
|
|
120
|
+
local reg wt branch work_dir purpose created status last_error dirty_summary head
|
|
119
121
|
reg="$(slot_registry_file "$agent_id")"
|
|
120
122
|
wt="$(existing_slot_worktree "$agent_id")"
|
|
121
123
|
branch="$(read_slot_field "$reg" branch || true)"
|
|
122
124
|
work_dir="$(read_slot_field "$reg" workDir || true)"
|
|
123
125
|
purpose="$(read_slot_field "$reg" purpose || true)"
|
|
124
126
|
created="$(read_slot_field "$reg" createdAt || true)"
|
|
127
|
+
status="$(read_slot_field "$reg" status || true)"
|
|
128
|
+
last_error="$(read_slot_field "$reg" lastError || true)"
|
|
125
129
|
dirty_summary="$(slot_dirty_summary "$wt")"
|
|
126
130
|
if [ -n "$wt" ] && [ -d "$wt" ]; then
|
|
127
131
|
head="$(git -C "$wt" rev-parse --short HEAD 2>/dev/null || true)"
|
|
@@ -133,6 +137,8 @@ print_slot_replace_warning() {
|
|
|
133
137
|
[ -n "$wt" ] && echo " Worktree: ${wt}" >&2
|
|
134
138
|
[ -n "$work_dir" ] && echo " Work dir: ${work_dir}" >&2
|
|
135
139
|
[ -n "$branch" ] && echo " Branch: ${branch}${head:+ @ ${head}}" >&2
|
|
140
|
+
[ -n "$status" ] && echo " Status: ${status}" >&2
|
|
141
|
+
[ -n "$last_error" ] && echo " Error: ${last_error}" >&2
|
|
136
142
|
[ -n "$created" ] && echo " Since: ${created}" >&2
|
|
137
143
|
echo " Git: ${dirty_summary}" >&2
|
|
138
144
|
echo "" >&2
|
|
@@ -182,11 +188,30 @@ require_slot_replace_confirm() {
|
|
|
182
188
|
exit 2
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
git_common_dir() {
|
|
192
|
+
local cwd="$1"
|
|
193
|
+
local out
|
|
194
|
+
out="$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null)" || return 1
|
|
195
|
+
case "$out" in
|
|
196
|
+
/*) echo "$out" ;;
|
|
197
|
+
*) (cd "$cwd" && cd "$out" && pwd) ;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
same_git_checkout() {
|
|
202
|
+
local left right
|
|
203
|
+
left="$(git_common_dir "$1" || true)"
|
|
204
|
+
right="$(git_common_dir "$2" || true)"
|
|
205
|
+
[ -n "$left" ] && [ -n "$right" ] && [ "$left" = "$right" ]
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
# Echo the previous session's worktree path for a slot: registry first, then
|
|
186
|
-
#
|
|
209
|
+
# legacy and randomized session worktree fallbacks. Empty output when none exists.
|
|
187
210
|
existing_slot_worktree() {
|
|
188
211
|
local agent_id="$1"
|
|
189
|
-
local reg path
|
|
212
|
+
local reg path candidate repo_root rel_prefix
|
|
213
|
+
repo_root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
214
|
+
rel_prefix="$(git -C "$repo_root" rev-parse --show-prefix 2>/dev/null || true)"
|
|
190
215
|
reg="$(slot_registry_file "$agent_id")"
|
|
191
216
|
if [ -f "$reg" ]; then
|
|
192
217
|
path="$(read_slot_field "$reg" worktreePath || true)"
|
|
@@ -198,12 +223,20 @@ existing_slot_worktree() {
|
|
|
198
223
|
path="$HOME/worktrees/${HARNESS_PROJECT_NAME}-agent-${agent_id}"
|
|
199
224
|
if [ -d "$path" ]; then
|
|
200
225
|
echo "$path"
|
|
226
|
+
return 0
|
|
201
227
|
fi
|
|
228
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*; do
|
|
229
|
+
[ -d "$candidate" ] || continue
|
|
230
|
+
same_git_checkout "$repo_root" "$candidate" || continue
|
|
231
|
+
[ -f "$candidate/${rel_prefix}.env.agent.${agent_id}" ] || continue
|
|
232
|
+
echo "$candidate"
|
|
233
|
+
return 0
|
|
234
|
+
done
|
|
202
235
|
return 0
|
|
203
236
|
}
|
|
204
237
|
|
|
205
|
-
# Resolve .env.agent.<id> — registry work dir first, then legacy
|
|
206
|
-
#
|
|
238
|
+
# Resolve .env.agent.<id> — registry work dir first, then legacy and
|
|
239
|
+
# randomized session worktree fallbacks.
|
|
207
240
|
resolve_agent_env_file() {
|
|
208
241
|
local agent_id="$1"
|
|
209
242
|
local repo_root="$2"
|
|
@@ -229,6 +262,15 @@ resolve_agent_env_file() {
|
|
|
229
262
|
return 0
|
|
230
263
|
fi
|
|
231
264
|
done
|
|
265
|
+
local candidate_dir
|
|
266
|
+
for candidate in "$HOME"/worktrees/*-har-agent-"${agent_id}"-*/${rel_prefix}.env.agent."${agent_id}"; do
|
|
267
|
+
if [ -f "$candidate" ]; then
|
|
268
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
269
|
+
same_git_checkout "$repo_root" "$candidate_dir" || continue
|
|
270
|
+
echo "$candidate"
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
done
|
|
232
274
|
return 1
|
|
233
275
|
}
|
|
234
276
|
|
|
@@ -278,3 +320,14 @@ run_browser_e2e_if_present() {
|
|
|
278
320
|
"$e2e" "$agent_id"
|
|
279
321
|
fi
|
|
280
322
|
}
|
|
323
|
+
|
|
324
|
+
# Optional project-owned "agent usable" smoke beyond health.
|
|
325
|
+
run_readiness_if_configured() {
|
|
326
|
+
local agent_id="$1"
|
|
327
|
+
if [ -z "${HARNESS_READINESS_CMD:-}" ]; then
|
|
328
|
+
echo "No HARNESS_READINESS_CMD configured; skipping readiness smoke."
|
|
329
|
+
return 0
|
|
330
|
+
fi
|
|
331
|
+
local cmd="${HARNESS_READINESS_CMD//\{agentId\}/$agent_id}"
|
|
332
|
+
eval "$cmd"
|
|
333
|
+
}
|
|
@@ -94,6 +94,46 @@ WORKTREE_DIR=${WORKTREE_DIR}
|
|
|
94
94
|
NODE_ENV=test
|
|
95
95
|
EOF
|
|
96
96
|
|
|
97
|
+
REGISTRY_WRITTEN=false
|
|
98
|
+
mark_slot_failed() {
|
|
99
|
+
local exit_code="$?"
|
|
100
|
+
if [ "$exit_code" != "0" ] && [ "$REGISTRY_WRITTEN" = true ]; then
|
|
101
|
+
log "Launch failed after creating the session. Recording failed slot state..."
|
|
102
|
+
set +e
|
|
103
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
104
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
105
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
106
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
107
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
108
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
109
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
110
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
111
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
112
|
+
SLOT_STATUS="failed" \
|
|
113
|
+
SLOT_LAST_ERROR="launch.sh exited with code ${exit_code}" \
|
|
114
|
+
write_slot_registry
|
|
115
|
+
log " Work dir: ${WORK_DIR}"
|
|
116
|
+
log " Env file: ${ENV_FILE}"
|
|
117
|
+
log " Recovery: fix the failure, then relaunch with --replace when ready."
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
trap mark_slot_failed EXIT
|
|
121
|
+
|
|
122
|
+
# Record the session before slow or fragile setup so verify/status/teardown can
|
|
123
|
+
# recover partial launches that already created a worktree and env file.
|
|
124
|
+
SLOT_AGENT_ID="$AGENT_ID" \
|
|
125
|
+
SLOT_MODE="$([ "$USE_WORKTREE" = true ] && echo worktree || echo root)" \
|
|
126
|
+
SLOT_WORK_DIR="$WORK_DIR" \
|
|
127
|
+
SLOT_SUFFIX="${SUFFIX:-}" \
|
|
128
|
+
SLOT_WORKTREE_PATH="${WORKTREE_DIR:-}" \
|
|
129
|
+
SLOT_BRANCH="${BRANCH:-}" \
|
|
130
|
+
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
131
|
+
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
132
|
+
SLOT_PURPOSE="${PURPOSE}" \
|
|
133
|
+
SLOT_STATUS="starting" \
|
|
134
|
+
write_slot_registry
|
|
135
|
+
REGISTRY_WRITTEN=true
|
|
136
|
+
|
|
97
137
|
if [ -f "$WORK_DIR/package.json" ]; then
|
|
98
138
|
log "Installing dependencies..."
|
|
99
139
|
(cd "$WORK_DIR" && npm install --silent)
|
|
@@ -118,6 +158,7 @@ SLOT_BRANCH="${BRANCH:-}" \
|
|
|
118
158
|
SLOT_BASE_BRANCH="${BASE_BRANCH:-}" \
|
|
119
159
|
SLOT_BASE_COMMIT="${BASE_COMMIT:-}" \
|
|
120
160
|
SLOT_PURPOSE="${PURPOSE}" \
|
|
161
|
+
SLOT_STATUS="active" \
|
|
121
162
|
write_slot_registry
|
|
122
163
|
|
|
123
164
|
log "Agent $AGENT_ID is ready."
|
|
@@ -38,6 +38,14 @@ set +a
|
|
|
38
38
|
WORK_DIR="$(resolve_agent_work_dir "$ENV_FILE")"
|
|
39
39
|
|
|
40
40
|
echo "==> Verifying agent ${AGENT_ID} in ${WORK_DIR}..." >&2
|
|
41
|
+
REG_FILE="$(slot_registry_file "$AGENT_ID")"
|
|
42
|
+
echo " Work dir: ${WORK_DIR}" >&2
|
|
43
|
+
echo " Env file: ${ENV_FILE}" >&2
|
|
44
|
+
if [ -f "$REG_FILE" ]; then
|
|
45
|
+
echo " Registry: ${REG_FILE}" >&2
|
|
46
|
+
else
|
|
47
|
+
echo " Registry: missing (${REG_FILE})" >&2
|
|
48
|
+
fi
|
|
41
49
|
|
|
42
50
|
# Build xcodebuild project/workspace flags
|
|
43
51
|
xc_target_flags() {
|
|
@@ -139,6 +147,8 @@ if [ -n "$FULL" ]; then
|
|
|
139
147
|
run_step "lint" "command -v \"${HARNESS_SWIFTLINT_CMD:-swiftlint}\" >/dev/null 2>&1 && \
|
|
140
148
|
\"${HARNESS_SWIFTLINT_CMD:-swiftlint}\" --quiet 2>&1 || echo 'swiftlint not installed — skipping'" || true
|
|
141
149
|
|
|
150
|
+
run_step "readiness" "run_readiness_if_configured \"$AGENT_ID\"" || true
|
|
151
|
+
|
|
142
152
|
# RocketSim user-flow validation — installed via: har env add-stage rocketsim
|
|
143
153
|
run_rocketsim_flows_if_present "$SCRIPT_DIR" "$AGENT_ID" || true
|
|
144
154
|
fi
|