okstra 0.155.0 → 0.157.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/docs/architecture.md +3 -1
- package/docs/for-ai/skills/okstra-schedule-gen.md +5 -4
- package/docs/project-structure-overview.md +7 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/python/okstra_ctl/clarification_items.py +3 -3
- package/runtime/python/okstra_ctl/render_final_report.py +31 -44
- package/runtime/python/okstra_ctl/report_contract.py +15 -0
- package/runtime/python/okstra_ctl/report_finalize.py +22 -3
- package/runtime/python/okstra_ctl/report_markdown.py +441 -0
- package/runtime/python/okstra_ctl/schedule_semantics.py +186 -91
- package/runtime/python/okstra_ctl/stage_map.py +203 -1
- package/runtime/python/okstra_ctl/wizard.py +1 -11
- package/runtime/python/okstra_project/state.py +14 -2
- package/runtime/skills/okstra-schedule-gen/SKILL.md +43 -18
- package/runtime/templates/reports/final-report-v2.template.md +74 -10
- package/runtime/templates/reports/md/macros/sections.md +19 -0
- package/runtime/templates/reports/md/tasks/change-impact-analysis.template.md +18 -0
- package/runtime/templates/reports/md/tasks/error-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/feature-analysis.template.md +13 -0
- package/runtime/templates/reports/md/tasks/final-verification.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +15 -0
- package/runtime/templates/reports/md/tasks/implementation.template.md +15 -0
- package/runtime/templates/reports/md/tasks/improvement-discovery.template.md +10 -0
- package/runtime/templates/reports/md/tasks/project-analysis.template.md +15 -0
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +13 -0
- package/runtime/templates/reports/md/tasks/requirements-discovery.template.md +15 -0
- package/runtime/templates/reports/schedule.template.md +166 -63
- package/runtime/validators/validate-run.py +19 -6
- package/runtime/validators/validate-schedule.py +94 -65
- package/src/commands/inspect/stage-map.mjs +6 -1
- package/src/commands/inspect/worker-liveness.mjs +15 -3
- package/src/commands/lifecycle/install.mjs +69 -4
- package/src/commands/lifecycle/uninstall.mjs +21 -35
- package/src/lib/install-assets.mjs +37 -0
|
@@ -5,6 +5,17 @@ const USAGE = `okstra worker-liveness — report whether pending workers are sti
|
|
|
5
5
|
Usage:
|
|
6
6
|
okstra worker-liveness [--team-state <path> --worker <worker-id>]...
|
|
7
7
|
[--max-idle <seconds>] [--launch-grace <seconds>] [--json]
|
|
8
|
+
okstra worker-liveness --wait [--team-state <path> --worker <worker-id>]...
|
|
9
|
+
[--interval <seconds>] [--timeout <seconds>]
|
|
10
|
+
|
|
11
|
+
--wait polls until every named worker's persisted resultPath lands (exit 0), one
|
|
12
|
+
worker probes unhealthy (exit 1), or --timeout passes (exit 2).
|
|
13
|
+
Defaults: --interval 20, --timeout 2400. Run it as a background
|
|
14
|
+
command and branch on the exit code. Use this instead of writing a
|
|
15
|
+
poll loop: both the "is it done" test (the persisted resultPath, not a
|
|
16
|
+
filename you assemble) and the "is it dead" test live in here, and a
|
|
17
|
+
hand-written loop re-derives them and gets one wrong. A worker row
|
|
18
|
+
without a resultPath is refused rather than waited on by guess.
|
|
8
19
|
|
|
9
20
|
--team-state and --worker select one pending worker. The worker row's
|
|
10
21
|
livenessMode picks the probe: audit-heartbeat reads the in-process
|
|
@@ -15,9 +26,10 @@ Usage:
|
|
|
15
26
|
is reused on re-dispatch, so the previous attempt's last heartbeat is
|
|
16
27
|
not this dispatch's signal.
|
|
17
28
|
|
|
18
|
-
Output: JSON { ok, checkedAt, probes[], unhealthy[] }
|
|
19
|
-
|
|
20
|
-
it reports, it never
|
|
29
|
+
Output: JSON { ok, checkedAt, probes[], unhealthy[] }, plus { outcome, pending[],
|
|
30
|
+
waitedSeconds } under --wait. Exit 1 when any worker is stalled or did not
|
|
31
|
+
launch, so a caller can branch on the exit code. Read-only: it reports, it never
|
|
32
|
+
kills or re-dispatches.
|
|
21
33
|
`;
|
|
22
34
|
|
|
23
35
|
export async function run(args) {
|
|
@@ -8,10 +8,13 @@ import { fileExists } from "../../lib/proc.mjs";
|
|
|
8
8
|
import { buildRuntimeManifest, RUNTIMES_MANIFEST_REL } from "../../lib/runtime-manifest.mjs";
|
|
9
9
|
import { normalizeRuntimeRequest, resolveRuntime } from "../../lib/runtime-resolver.mjs";
|
|
10
10
|
import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
|
|
11
|
+
import {
|
|
12
|
+
AGENTS_MANIFEST_REL,
|
|
13
|
+
SKILLS_MANIFEST_REL,
|
|
14
|
+
} from "../../lib/install-assets.mjs";
|
|
11
15
|
import { renderWorkerAgentDefinitions } from "../../lib/worker-agent-render.mjs";
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
const AGENTS_MANIFEST_REL = "installed-agents.json";
|
|
17
|
+
|
|
15
18
|
const USER_HOME = homedir();
|
|
16
19
|
const CLAUDE_HOME = join(USER_HOME, ".claude");
|
|
17
20
|
const AGENTS_HOME = join(USER_HOME, ".agents");
|
|
@@ -236,15 +239,42 @@ async function writeFileAtomic(target, data, mode) {
|
|
|
236
239
|
await fs.rename(tmp, target);
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
// Files the destination has and the payload no longer ships. Left in place they
|
|
243
|
+
// are indistinguishable from current code — Python imports whatever is on disk,
|
|
244
|
+
// so a module dropped in release N+1 keeps being importable after the upgrade,
|
|
245
|
+
// and a rename leaves both spellings live at once.
|
|
246
|
+
async function pruneOrphans(srcRoot, dstRoot, opts) {
|
|
247
|
+
const { dryRun = false } = opts ?? {};
|
|
248
|
+
if (!(await dirExists(dstRoot))) return 0;
|
|
249
|
+
let pruned = 0;
|
|
250
|
+
for await (const dstPath of walkFiles(dstRoot)) {
|
|
251
|
+
const rel = relative(dstRoot, dstPath);
|
|
252
|
+
try {
|
|
253
|
+
await fs.access(join(srcRoot, rel));
|
|
254
|
+
continue;
|
|
255
|
+
} catch {
|
|
256
|
+
// not in the payload → orphan
|
|
257
|
+
}
|
|
258
|
+
if (dryRun) {
|
|
259
|
+
process.stdout.write(`[dry-run] prune ${dstPath}\n`);
|
|
260
|
+
} else {
|
|
261
|
+
await fs.rm(dstPath, { force: true });
|
|
262
|
+
}
|
|
263
|
+
pruned++;
|
|
264
|
+
}
|
|
265
|
+
return pruned;
|
|
266
|
+
}
|
|
267
|
+
|
|
239
268
|
async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
|
|
240
269
|
const { refresh = false, dryRun = false, mode } = opts ?? {};
|
|
241
270
|
let copied = 0;
|
|
242
271
|
let skipped = 0;
|
|
272
|
+
let pruned = 0;
|
|
243
273
|
let missingSource = false;
|
|
244
274
|
|
|
245
275
|
if (!(await dirExists(srcRoot))) {
|
|
246
276
|
missingSource = true;
|
|
247
|
-
return { copied, skipped, missingSource };
|
|
277
|
+
return { copied, skipped, pruned, missingSource };
|
|
248
278
|
}
|
|
249
279
|
|
|
250
280
|
for await (const srcPath of walkFiles(srcRoot)) {
|
|
@@ -276,7 +306,8 @@ async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
|
|
|
276
306
|
copied++;
|
|
277
307
|
}
|
|
278
308
|
|
|
279
|
-
|
|
309
|
+
pruned = await pruneOrphans(srcRoot, dstRoot, { dryRun });
|
|
310
|
+
return { copied, skipped, pruned, missingSource };
|
|
280
311
|
}
|
|
281
312
|
|
|
282
313
|
async function ensureSymlink(target, linkPath, opts) {
|
|
@@ -970,6 +1001,23 @@ function summarise(label, result, target) {
|
|
|
970
1001
|
);
|
|
971
1002
|
}
|
|
972
1003
|
|
|
1004
|
+
// `0.156.0` → [0, 156, 0]. Anything else → null, so a caller falls back to the
|
|
1005
|
+
// plain inequality test rather than guessing an ordering.
|
|
1006
|
+
function parseSemver(value) {
|
|
1007
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(value ?? "").trim());
|
|
1008
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function installedStampIsNewer(stamp, packageVersion) {
|
|
1012
|
+
const installed = parseSemver(stamp);
|
|
1013
|
+
const running = parseSemver(packageVersion);
|
|
1014
|
+
if (!installed || !running) return false;
|
|
1015
|
+
for (let i = 0; i < 3; i++) {
|
|
1016
|
+
if (installed[i] !== running[i]) return installed[i] > running[i];
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
973
1021
|
export function parseEnsureInstalledArgs(args) {
|
|
974
1022
|
const result = {
|
|
975
1023
|
quiet: false,
|
|
@@ -1009,6 +1057,23 @@ export async function runEnsureInstalled(args) {
|
|
|
1009
1057
|
return 2;
|
|
1010
1058
|
}
|
|
1011
1059
|
|
|
1060
|
+
// A newer install than the package running this check means an upgrade
|
|
1061
|
+
// arrived by another route — `npx -y okstra@latest install`, or an install
|
|
1062
|
+
// from a checkout — while this command still resolves an older payload.
|
|
1063
|
+
// Reinstalling here would quietly DOWNGRADE the user's runtime, and every
|
|
1064
|
+
// skill preflight calls this, so the downgrade lands on the next command
|
|
1065
|
+
// after the upgrade. Refuse and say so; an explicit `okstra install` is
|
|
1066
|
+
// still honoured, because that one is the user's own decision.
|
|
1067
|
+
if (installedStampIsNewer(paths.version, paths.package)) {
|
|
1068
|
+
process.stderr.write(
|
|
1069
|
+
`okstra: installed runtime ${paths.version} is newer than this package ` +
|
|
1070
|
+
`(${paths.package}) — skipping auto-install so it is not downgraded. ` +
|
|
1071
|
+
`Run 'okstra install' explicitly to force this package's payload, or ` +
|
|
1072
|
+
`update the okstra package itself.\n`,
|
|
1073
|
+
);
|
|
1074
|
+
return 0;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1012
1077
|
const reasons = [];
|
|
1013
1078
|
if (!paths.version) reasons.push("no version stamp");
|
|
1014
1079
|
else if (paths.version !== paths.package) {
|
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
runtimeManifestIncludesClaudeAssets,
|
|
8
8
|
} from "../../lib/runtime-manifest.mjs";
|
|
9
9
|
import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
|
|
10
|
+
import {
|
|
11
|
+
AGENTS_MANIFEST_REL,
|
|
12
|
+
INSTALLED_FILES,
|
|
13
|
+
INSTALLED_TREES,
|
|
14
|
+
SKILLS_MANIFEST_REL,
|
|
15
|
+
} from "../../lib/install-assets.mjs";
|
|
10
16
|
|
|
11
17
|
// Manifest-less uninstall must clean out every okstra skill directory — the
|
|
12
18
|
// live names AND every former/renamed one. Both sets are the single source of
|
|
@@ -34,8 +40,7 @@ const FALLBACK_SKILL_ROOTS = [
|
|
|
34
40
|
CLAUDE_SKILLS_DIR,
|
|
35
41
|
AGENTS_SKILLS_DIR,
|
|
36
42
|
];
|
|
37
|
-
|
|
38
|
-
const AGENTS_MANIFEST_REL = "installed-agents.json";
|
|
43
|
+
|
|
39
44
|
|
|
40
45
|
const USAGE = `okstra uninstall — remove installed runtime and okstra skills
|
|
41
46
|
|
|
@@ -147,21 +152,16 @@ export async function runUninstall(args) {
|
|
|
147
152
|
process.stdout.write(`uninstalling okstra runtime\n`);
|
|
148
153
|
process.stdout.write(` home: ${paths.home}\n`);
|
|
149
154
|
}
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// a stale
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// Remove the directory wholesale, matching how install populates it; nothing
|
|
161
|
-
// under ~/.okstra/bin is user data.
|
|
162
|
-
await removePath(paths.bin, opts);
|
|
163
|
-
// Clean now-empty lib parent (best-effort) — pythonpath + validators were the
|
|
164
|
-
// only okstra-owned children.
|
|
155
|
+
// Every tree the install writes, read from the list install itself copies
|
|
156
|
+
// from. A per-name list here drifts behind additions the moment one lands on
|
|
157
|
+
// the install side only — which is how a stale validator, a stale schema and
|
|
158
|
+
// a stale prompts tree each survived uninstall→reinstall in turn, one bug
|
|
159
|
+
// report at a time.
|
|
160
|
+
for (const tree of INSTALLED_TREES) {
|
|
161
|
+
await removePath(join(paths.home, ...tree.to), opts);
|
|
162
|
+
}
|
|
163
|
+
// Clean now-empty lib parent (best-effort) — the trees above were the only
|
|
164
|
+
// okstra-owned children.
|
|
165
165
|
const libDir = join(paths.home, "lib");
|
|
166
166
|
if (await pathExists(libDir)) {
|
|
167
167
|
try {
|
|
@@ -175,15 +175,6 @@ export async function runUninstall(args) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// schemas/ tree — installed by copy mode (runtime/schemas). Older uninstall
|
|
179
|
-
// never removed it, leaving a stale final-report schema behind.
|
|
180
|
-
await removePath(join(paths.home, "schemas"), opts);
|
|
181
|
-
|
|
182
|
-
// prompts/ tree — installed by copy mode (runtime/prompts): lead contracts
|
|
183
|
-
// + coding-preflight resource pack. Remove so an upgrade never serves stale
|
|
184
|
-
// operating contracts.
|
|
185
|
-
await removePath(join(paths.home, "prompts"), opts);
|
|
186
|
-
|
|
187
178
|
// Remove the skills we own. Manifest v2 records every target root; legacy
|
|
188
179
|
// manifests fall back to Claude-only names when the runtime manifest says
|
|
189
180
|
// Claude assets may have been installed.
|
|
@@ -210,20 +201,15 @@ export async function runUninstall(args) {
|
|
|
210
201
|
for (const name of agentNames) {
|
|
211
202
|
await removePath(join(CLAUDE_AGENTS_DIR, name), opts);
|
|
212
203
|
}
|
|
213
|
-
await removePath(join(paths.home, AGENTS_MANIFEST_REL), opts);
|
|
214
|
-
|
|
215
|
-
// templates/ tree — installed wholesale by copy mode (runtime/templates),
|
|
216
|
-
// including the seeded settings.local.json sidecar. Remove the whole tree so
|
|
217
|
-
// an upgrade/reinstall never serves a stale report.css / *.template.md.
|
|
218
|
-
await removePath(join(paths.home, "templates"), opts);
|
|
219
204
|
// Per-project <PROJECT>/.claude/settings.local.json symlinks are NOT removed
|
|
220
205
|
// here — uninstall is machine-level and does not know which projects opted
|
|
221
206
|
// in. They will dangle until the user removes them manually or re-runs
|
|
222
207
|
// okstra install + okstra setup.
|
|
223
208
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
// Stamps and manifests, last: the skill/agent removal above reads two of them.
|
|
210
|
+
for (const name of INSTALLED_FILES) {
|
|
211
|
+
await removePath(join(paths.home, name), opts);
|
|
212
|
+
}
|
|
227
213
|
|
|
228
214
|
if (!opts.quiet) {
|
|
229
215
|
process.stdout.write("done. user data preserved (recent.jsonl, projects/, archive/, ...).\n");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// What `okstra install` puts under ~/.okstra, as one list.
|
|
2
|
+
//
|
|
3
|
+
// install and uninstall each used to carry their own copy of this, so an asset
|
|
4
|
+
// added to one and not the other survived every uninstall — `installed-from`
|
|
5
|
+
// sat in real homes for months that way, written by a version that no longer
|
|
6
|
+
// exists and removed by nothing.
|
|
7
|
+
//
|
|
8
|
+
// Skills and agents are NOT here: they install outside ~/.okstra (host skill
|
|
9
|
+
// homes, ~/.claude/agents) and are tracked per-target by their own manifests.
|
|
10
|
+
|
|
11
|
+
// runtime/<from> is mirrored to ~/.okstra/<to...>. Mirrored, not merged: a file
|
|
12
|
+
// the payload no longer ships is removed from the install, because Python
|
|
13
|
+
// imports whatever is on disk and a stale module outlives the release that
|
|
14
|
+
// dropped it.
|
|
15
|
+
export const INSTALLED_TREES = [
|
|
16
|
+
{ from: "python", to: ["lib", "python"] },
|
|
17
|
+
{ from: "bin", to: ["bin"] },
|
|
18
|
+
{ from: "templates", to: ["templates"] },
|
|
19
|
+
{ from: "schemas", to: ["schemas"] },
|
|
20
|
+
{ from: "validators", to: ["lib", "validators"] },
|
|
21
|
+
{ from: "prompts", to: ["prompts"] },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Single files the install stamps into ~/.okstra. Every one is removed by
|
|
25
|
+
// uninstall; `installed-from` is written by no current code path and is listed
|
|
26
|
+
// so that homes carrying it from an older version still get cleaned.
|
|
27
|
+
export const INSTALLED_FILES = [
|
|
28
|
+
"version",
|
|
29
|
+
"dev-link",
|
|
30
|
+
"installed-from",
|
|
31
|
+
"installed-skills.json",
|
|
32
|
+
"installed-agents.json",
|
|
33
|
+
"installed-runtimes.json",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const SKILLS_MANIFEST_REL = "installed-skills.json";
|
|
37
|
+
export const AGENTS_MANIFEST_REL = "installed-agents.json";
|