@workit-dev/core 0.0.1 → 1.0.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/LICENSE +21 -0
- package/README.md +22 -43
- package/package.json +56 -6
- package/schemas/config.json +74 -0
- package/schemas/vcs.json +79 -0
- package/schemas/workspaces.json +1029 -0
- package/schemas/youtrack.json +84 -0
- package/scripts/analyze-release-scope.ts +215 -0
- package/scripts/bundled-deps.json +131 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +146 -0
- package/scripts/install-opencode-plugin.sh +79 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/lib/config-dir.sh +28 -0
- package/scripts/publish-changed-packages.ts +395 -0
- package/scripts/rewrite-workspace-deps.ts +53 -0
- package/scripts/run-cursor-mcp.sh +6 -0
- package/scripts/sync-release-manifests.ts +82 -0
- package/scripts/sync-runtime.sh +142 -0
- package/scripts/validate-cursor-marketplace.ts +171 -0
- package/scripts/verify-bundle-sources.ts +134 -0
- package/scripts/write-schemas.ts +16 -0
- package/skills/workit-architecture/SKILL.md +72 -0
- package/skills/workit-architecture/agents/openai.yaml +3 -0
- package/skills/workit-architecture/references/instruction-files.md +52 -0
- package/skills/workit-architecture/references/vocabulary.md +53 -0
- package/skills/workit-bdd/SKILL.md +53 -0
- package/skills/workit-continue/SKILL.md +54 -0
- package/skills/workit-debug/SKILL.md +45 -0
- package/skills/workit-deslop/SKILL.md +58 -0
- package/skills/workit-fanout/SKILL.md +71 -0
- package/skills/workit-fanout/references/brief.md +141 -0
- package/skills/workit-fanout/references/orchestration.md +169 -0
- package/skills/workit-implement/SKILL.md +71 -0
- package/skills/workit-retro/SKILL.md +72 -0
- package/skills/workit-retro/agents/openai.yaml +3 -0
- package/skills/workit-retro/references/sources.md +35 -0
- package/skills/workit-retro/references/steering.md +39 -0
- package/skills/workit-review/SKILL.md +66 -0
- package/skills/workit-review/references/impact.md +24 -0
- package/skills/workit-shape/SKILL.md +71 -0
- package/skills/workit-shape/references/diagrams.md +17 -0
- package/skills/workit-shape/references/knowledge.md +61 -0
- package/skills/workit-shape/references/mockups.md +15 -0
- package/skills/workit-shape/references/slicing.md +44 -0
- package/skills/workit-ship/SKILL.md +71 -0
- package/skills/workit-ship/references/changelog.md +44 -0
- package/skills/workit-ship/references/pr-body.md +32 -0
- package/skills/workit-ship/references/release.md +39 -0
- package/skills/workit-test-audit/SKILL.md +54 -0
- package/skills/workit-verify-app/SKILL.md +68 -0
- package/skills/workit-verify-app/references/template.md +51 -0
- package/src/autonomy.ts +910 -0
- package/src/check-config.ts +613 -0
- package/src/checks.ts +534 -0
- package/src/ci-waiter.ts +159 -0
- package/src/core/boundary.ts +55 -0
- package/src/core/branch-policy.ts +112 -0
- package/src/core/branch.ts +1192 -0
- package/src/core/changelog-gate.ts +349 -0
- package/src/core/changelog-merge.ts +214 -0
- package/src/core/changelog.ts +392 -0
- package/src/core/cleanup.ts +638 -0
- package/src/core/cli-locate.ts +173 -0
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-schemas.ts +653 -0
- package/src/core/config.ts +351 -0
- package/src/core/context-read.ts +144 -0
- package/src/core/docs-gate.ts +69 -0
- package/src/core/finding-evidence.ts +89 -0
- package/src/core/finding-queue.ts +36 -0
- package/src/core/git.ts +73 -0
- package/src/core/gitignore.ts +52 -0
- package/src/core/heavy-eta.ts +170 -0
- package/src/core/heavy-lease.ts +290 -0
- package/src/core/heavy-rules.ts +373 -0
- package/src/core/heavy-scope.ts +131 -0
- package/src/core/heavy-words.ts +83 -0
- package/src/core/heavy.ts +535 -0
- package/src/core/hygiene.ts +149 -0
- package/src/core/init.ts +218 -0
- package/src/core/live-session.ts +333 -0
- package/src/core/logger.ts +310 -0
- package/src/core/methods.ts +185 -0
- package/src/core/model-fallback.ts +208 -0
- package/src/core/node-resolve.mjs +249 -0
- package/src/core/operation-input.ts +632 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/policy/attributes.ts +97 -0
- package/src/core/policy/bookkeeping.ts +119 -0
- package/src/core/policy/carry.ts +122 -0
- package/src/core/policy/check-evidence.ts +115 -0
- package/src/core/policy/current-judgment.ts +20 -0
- package/src/core/policy/derive.ts +387 -0
- package/src/core/policy/judgment.ts +246 -0
- package/src/core/policy/lane.ts +562 -0
- package/src/core/policy/pr-lane.ts +32 -0
- package/src/core/policy/substance.ts +365 -0
- package/src/core/policy-resolver.ts +94 -0
- package/src/core/pr-create.ts +448 -0
- package/src/core/pr-template.ts +162 -0
- package/src/core/release-notes.ts +197 -0
- package/src/core/release-tracks.ts +779 -0
- package/src/core/remotes.ts +436 -0
- package/src/core/repo-context.ts +542 -0
- package/src/core/repo-paths.ts +29 -0
- package/src/core/resources.ts +1299 -0
- package/src/core/role-models.ts +603 -0
- package/src/core/route-intent.ts +305 -0
- package/src/core/rules.ts +65 -0
- package/src/core/runtime-identity.ts +63 -0
- package/src/core/safe-write.ts +87 -0
- package/src/core/scope-glob.ts +56 -0
- package/src/core/scripts.ts +6 -0
- package/src/core/skill-manifests.ts +173 -0
- package/src/core/store-lock.ts +374 -0
- package/src/core/support-matrix.ts +24 -0
- package/src/core/task-context.ts +247 -0
- package/src/core/task-contract.ts +1279 -0
- package/src/core/task-delivery.ts +109 -0
- package/src/core/task-engine.ts +2248 -0
- package/src/core/task-evaluation.ts +1147 -0
- package/src/core/task-store.ts +2649 -0
- package/src/core/task-view.ts +180 -0
- package/src/core/templates.ts +22 -0
- package/src/core/tracker-issues.ts +174 -0
- package/src/core/vcs-config.ts +540 -0
- package/src/core/workers.ts +62 -0
- package/src/core/workit-paths.mjs +242 -0
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/core/workspaces.ts +1745 -0
- package/src/core/youtrack-backend.ts +288 -0
- package/src/core/youtrack-tools.ts +260 -0
- package/src/core/youtrack-writes.ts +73 -0
- package/src/core/youtrack-ytrack.ts +217 -0
- package/src/core/youtrack.ts +454 -0
- package/src/core/ytrack-skill.ts +297 -0
- package/src/core.ts +171 -0
- package/src/deslop/lex.ts +271 -0
- package/src/deslop/scan.ts +232 -0
- package/src/duration.ts +27 -0
- package/src/fanout-brief.ts +486 -0
- package/src/fanout-check.ts +455 -0
- package/src/fanout-landed.ts +370 -0
- package/src/fanout-stack.ts +108 -0
- package/src/fanout-status.ts +362 -0
- package/src/fanout-worktree.ts +893 -0
- package/src/fanout.ts +1473 -0
- package/src/forge/exec.ts +385 -0
- package/src/forge/github.ts +831 -0
- package/src/forge/gitlab.ts +906 -0
- package/src/forge/issue-forge.ts +209 -0
- package/src/forge/issue-link.ts +468 -0
- package/src/forge/issue-ops.ts +314 -0
- package/src/forge/merge-gate-base-red.ts +72 -0
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-edit.ts +292 -0
- package/src/forge/pr-ops.ts +1109 -0
- package/src/forge/pr-status.ts +10 -0
- package/src/forge/redact.ts +195 -0
- package/src/forge/report.ts +994 -0
- package/src/forge/reruns.ts +132 -0
- package/src/forge/resolve.ts +509 -0
- package/src/forge/types.ts +242 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/default-base.ts +130 -0
- package/src/git/env-redirect.ts +28 -0
- package/src/git/facts.ts +122 -0
- package/src/git/ops.ts +1823 -0
- package/src/git/refs-read.ts +332 -0
- package/src/git/release.ts +1020 -0
- package/src/git/rev.ts +1175 -0
- package/src/git/session-hook.ts +597 -0
- package/src/git/stage.ts +63 -0
- package/src/hooks/agent-model.ts +149 -0
- package/src/hooks/budget-signal.ts +284 -0
- package/src/hooks/context.ts +461 -0
- package/src/hooks/cwd-normalize.ts +79 -0
- package/src/hooks/deadline.ts +228 -0
- package/src/hooks/delivery-verbs.ts +182 -0
- package/src/hooks/descriptor.ts +124 -0
- package/src/hooks/handle.ts +433 -0
- package/src/hooks/heavy-gate.ts +99 -0
- package/src/hooks/hook-runs.ts +130 -0
- package/src/hooks/hosts/claude-code.ts +380 -0
- package/src/hooks/hosts/codex.ts +372 -0
- package/src/hooks/hosts/cursor.ts +419 -0
- package/src/hooks/hosts/fields.ts +98 -0
- package/src/hooks/hosts/opencode.ts +79 -0
- package/src/hooks/hosts/pi.ts +78 -0
- package/src/hooks/index.ts +60 -0
- package/src/hooks/model-limit.ts +378 -0
- package/src/hooks/model-switch.ts +35 -0
- package/src/hooks/policy.ts +29 -0
- package/src/hooks/process.ts +48 -0
- package/src/hooks/protocol.ts +135 -0
- package/src/hooks/raw-git.ts +2289 -0
- package/src/hooks/repeat-failure.ts +115 -0
- package/src/hooks/run.ts +54 -0
- package/src/hooks/session-marker.ts +151 -0
- package/src/hooks/shell-words.ts +532 -0
- package/src/hooks/skill-nudge.ts +295 -0
- package/src/hooks/spawn-gate.ts +72 -0
- package/src/hooks/standing-signal.ts +99 -0
- package/src/hooks/standing.ts +225 -0
- package/src/hooks/steering.ts +74 -0
- package/src/hooks/stop.ts +408 -0
- package/src/hooks/worktree-confine.ts +415 -0
- package/src/hooks/write-gate.ts +1174 -0
- package/src/host-session.ts +459 -0
- package/src/knowledge.ts +368 -0
- package/src/ledger.ts +2913 -0
- package/src/stack.ts +2570 -0
- package/src/store/event-log.ts +217 -0
- package/src/store/notes.ts +29 -0
- package/src/store/patch.ts +135 -0
- package/src/store/paths.ts +368 -0
- package/src/store/reduce.ts +203 -0
- package/src/test-audit-mocks.ts +309 -0
- package/src/verdict-cover.ts +151 -0
- package/src/worktree-deps.ts +142 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +5 -0
- package/templates/plan-template.md +26 -0
- package/templates/spec-template.md +64 -0
- package/templates/workit-contract.md +13 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// The ytrack agent skill (youtrack-cli #16): ytrack ships a skill stamped
|
|
2
|
+
// with its own version (`ytrack skill print`, `ytrack skill install --dir`).
|
|
3
|
+
// Workit runs that install into each agent host's native user skills
|
|
4
|
+
// directory and records what it installed, so uninstall removes exactly
|
|
5
|
+
// those directories. Workit never copies the skill's text.
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { issueTrackerOf } from "./vcs-config";
|
|
18
|
+
import { readWorkspacesResult } from "./workspaces";
|
|
19
|
+
import { ytrackDetail, type YtrackRun } from "./youtrack-ytrack";
|
|
20
|
+
|
|
21
|
+
export type SkillHost = "claude-code" | "codex" | "cursor" | "opencode" | "pi";
|
|
22
|
+
|
|
23
|
+
/** The skill's directory name and its frontmatter `name`. */
|
|
24
|
+
export const YTRACK_SKILL = "ytrack";
|
|
25
|
+
|
|
26
|
+
/** The ytrack that adds `ytrack skill`; an older one needs this. */
|
|
27
|
+
export const YTRACK_UPGRADE = "ytrack upgrade";
|
|
28
|
+
|
|
29
|
+
/** Whether Workit uses YouTrack: youtrack.json exists, or a workspace tracks issues in YouTrack. */
|
|
30
|
+
export const youTrackInUse = (configDir: string, env: NodeJS.ProcessEnv): boolean => {
|
|
31
|
+
if (existsSync(env.WORKIT_YOUTRACK_CONFIG ?? path.join(configDir, "youtrack.json"))) return true;
|
|
32
|
+
return readWorkspacesResult(configDir).entries.some((entry) => {
|
|
33
|
+
const raw = entry as Record<string, unknown>;
|
|
34
|
+
return issueTrackerOf(raw.issues, raw.youtrack)?.provider === "youtrack";
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const bare = (version: string): string => version.trim().replace(/^v/u, "");
|
|
39
|
+
|
|
40
|
+
/** Two versions name the same release (`v0.3.0` and `0.3.0` do). */
|
|
41
|
+
export const sameVersion = (a: string, b: string): boolean => bare(a) === bare(b);
|
|
42
|
+
|
|
43
|
+
/** A SKILL.md's frontmatter `name` and `metadata.version`. */
|
|
44
|
+
export const parseSkillMeta = (text: string): { name: string | null; version: string | null } => {
|
|
45
|
+
const lines = text.replace(/^/u, "").split(/\r?\n/u);
|
|
46
|
+
if (lines[0] !== "---") return { name: null, version: null };
|
|
47
|
+
const end = lines.indexOf("---", 1);
|
|
48
|
+
const front = end > 0 ? lines.slice(1, end) : [];
|
|
49
|
+
const unquote = (value: string) => value.trim().replace(/^(["'])(.*)\1$/u, "$2");
|
|
50
|
+
let name: string | null = null;
|
|
51
|
+
let version: string | null = null;
|
|
52
|
+
let inMetadata = false;
|
|
53
|
+
for (const line of front) {
|
|
54
|
+
if (/^\S/u.test(line)) inMetadata = /^metadata:\s*$/u.test(line);
|
|
55
|
+
const top = /^name:(.*)$/u.exec(line);
|
|
56
|
+
if (top) name = unquote(top[1]);
|
|
57
|
+
const nested = inMetadata ? /^\s+version:(.*)$/u.exec(line) : null;
|
|
58
|
+
if (nested) version = unquote(nested[1]);
|
|
59
|
+
}
|
|
60
|
+
return { name, version };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The ytrack skill installed at `dir` (`<root>/ytrack`): its version, or null when absent or not ytrack's. */
|
|
64
|
+
export const installedSkillVersion = (dir: string): string | null => {
|
|
65
|
+
try {
|
|
66
|
+
const meta = parseSkillMeta(readFileSync(path.join(dir, "SKILL.md"), "utf8"));
|
|
67
|
+
return meta.name === YTRACK_SKILL ? (meta.version ?? "") : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type YtrackSkillSupport =
|
|
74
|
+
| { state: "missing" }
|
|
75
|
+
| { state: "unsupported"; version: string | null }
|
|
76
|
+
| { state: "supported"; version: string };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether the ytrack on PATH ships the skill: `ytrack skill print` exits 0
|
|
80
|
+
* with the ytrack skill. An older ytrack (0.2.1 and before) has no `skill` command.
|
|
81
|
+
*/
|
|
82
|
+
export function ytrackSkillSupport(run: YtrackRun): YtrackSkillSupport {
|
|
83
|
+
const out = run(["version"]);
|
|
84
|
+
if (out.missing) return { state: "missing" };
|
|
85
|
+
const version = /ytrack version (\S+)/u.exec(out.stdout)?.[1] ?? null;
|
|
86
|
+
const print = run(["skill", "print"]);
|
|
87
|
+
const meta = print.status === 0 ? parseSkillMeta(print.stdout) : null;
|
|
88
|
+
if (!meta || meta.name !== YTRACK_SKILL) return { state: "unsupported", version };
|
|
89
|
+
const stamped = version ?? meta.version;
|
|
90
|
+
return stamped ? { state: "supported", version: stamped } : { state: "unsupported", version };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One skill install Workit made: uninstall removes `path` and only that. */
|
|
94
|
+
export type YtrackSkillInstall = { host: SkillHost; path: string; version: string };
|
|
95
|
+
|
|
96
|
+
const recordFile = (stateDir: string): string => path.join(stateDir, "ytrack-skill.json");
|
|
97
|
+
|
|
98
|
+
/** The skill installs Workit recorded, one per host. */
|
|
99
|
+
export const readYtrackSkillRecord = (stateDir: string): YtrackSkillInstall[] => {
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(readFileSync(recordFile(stateDir), "utf8")) as { installs?: unknown };
|
|
102
|
+
return (Array.isArray(data.installs) ? data.installs : []).filter(
|
|
103
|
+
(row): row is YtrackSkillInstall =>
|
|
104
|
+
typeof row === "object" &&
|
|
105
|
+
row !== null &&
|
|
106
|
+
typeof (row as YtrackSkillInstall).host === "string" &&
|
|
107
|
+
typeof (row as YtrackSkillInstall).path === "string" &&
|
|
108
|
+
typeof (row as YtrackSkillInstall).version === "string",
|
|
109
|
+
);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const writeRecord = (stateDir: string, installs: YtrackSkillInstall[]): void => {
|
|
116
|
+
const file = recordFile(stateDir);
|
|
117
|
+
if (installs.length === 0) {
|
|
118
|
+
rmSync(file, { force: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
122
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
123
|
+
writeFileSync(temp, JSON.stringify({ installs }, null, 2) + "\n", { mode: 0o600 });
|
|
124
|
+
renameSync(temp, file);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type YtrackSkillResult = {
|
|
128
|
+
host: SkillHost;
|
|
129
|
+
/** The skill directory (`<host skills root>/ytrack`). */
|
|
130
|
+
path: string;
|
|
131
|
+
/** installed: new or refreshed; current: already this version; failed: ytrack refused. */
|
|
132
|
+
status: "installed" | "current" | "failed";
|
|
133
|
+
/** The version found before this run, when a ytrack skill was there. */
|
|
134
|
+
previous: string | null;
|
|
135
|
+
error?: string;
|
|
136
|
+
/** The command that installs this host's skill by hand. */
|
|
137
|
+
command: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type YtrackSkillSync =
|
|
141
|
+
| { state: "missing" }
|
|
142
|
+
| { state: "unsupported"; version: string | null }
|
|
143
|
+
| { state: "synced"; version: string; results: YtrackSkillResult[] };
|
|
144
|
+
|
|
145
|
+
const quoteArg = (value: string): string =>
|
|
146
|
+
/^[\w./:@~+-]+$/u.test(value) ? value : `"${value.replaceAll('"', '\\"')}"`;
|
|
147
|
+
|
|
148
|
+
/** The command that installs the skill into `root` by hand. */
|
|
149
|
+
export const ytrackSkillCommand = (root: string): string =>
|
|
150
|
+
`ytrack skill install --dir ${quoteArg(root)}`;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Install the skill of the ytrack on PATH into each host's skills directory
|
|
154
|
+
* (`roots`: the host's native user skills directory)
|
|
155
|
+
* (`ytrack skill install --dir <root> --json`). A directory already at this
|
|
156
|
+
* version is left alone. Workit records a directory it created, and keeps
|
|
157
|
+
* recording one it created before; a ytrack skill someone else put there is
|
|
158
|
+
* refreshed but never recorded, so uninstall leaves it.
|
|
159
|
+
*/
|
|
160
|
+
export function syncYtrackSkill(options: {
|
|
161
|
+
roots: readonly { host: SkillHost; root: string }[];
|
|
162
|
+
stateDir: string;
|
|
163
|
+
run: YtrackRun;
|
|
164
|
+
}): YtrackSkillSync {
|
|
165
|
+
const support = ytrackSkillSupport(options.run);
|
|
166
|
+
if (support.state !== "supported") return support;
|
|
167
|
+
const recorded = readYtrackSkillRecord(options.stateDir);
|
|
168
|
+
const results: YtrackSkillResult[] = [];
|
|
169
|
+
for (const { host, root } of options.roots) {
|
|
170
|
+
const dir = path.join(root, YTRACK_SKILL);
|
|
171
|
+
const command = ytrackSkillCommand(root);
|
|
172
|
+
const previous = installedSkillVersion(dir);
|
|
173
|
+
const ours = recorded.find((row) => row.host === host && row.path === dir);
|
|
174
|
+
if (previous !== null && sameVersion(previous, support.version)) {
|
|
175
|
+
results.push({ host, path: dir, status: "current", previous, command });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const existed = previous !== null || existsSync(dir) || isLink(dir);
|
|
179
|
+
const out = options.run(["skill", "install", "--dir", root, "--json"]);
|
|
180
|
+
let installed: { path?: unknown; version?: unknown } | null = null;
|
|
181
|
+
try {
|
|
182
|
+
installed = out.status === 0 ? JSON.parse(out.stdout) : null;
|
|
183
|
+
} catch {
|
|
184
|
+
installed = null;
|
|
185
|
+
}
|
|
186
|
+
if (!installed || typeof installed.version !== "string") {
|
|
187
|
+
results.push({
|
|
188
|
+
host,
|
|
189
|
+
path: dir,
|
|
190
|
+
status: "failed",
|
|
191
|
+
previous,
|
|
192
|
+
error: ytrackDetail(out) || `ytrack exited ${out.status ?? "without a status"}`,
|
|
193
|
+
command,
|
|
194
|
+
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
results.push({ host, path: dir, status: "installed", previous, command });
|
|
198
|
+
if (ours || !existed) {
|
|
199
|
+
const rest = recorded.filter((row) => row.host !== host);
|
|
200
|
+
recorded.splice(0, recorded.length, ...rest, {
|
|
201
|
+
host,
|
|
202
|
+
path: dir,
|
|
203
|
+
version: installed.version,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
writeRecord(options.stateDir, recorded);
|
|
208
|
+
return { state: "synced", version: support.version, results };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const isLink = (file: string): boolean => {
|
|
212
|
+
try {
|
|
213
|
+
return lstatSync(file).isSymbolicLink();
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const realpathOrNull = (file: string): string | null => {
|
|
220
|
+
try {
|
|
221
|
+
return realpathSync.native(file);
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Why a recorded skill path is not the one Workit may delete, or null when it
|
|
229
|
+
* is. The only allowed path is `<root>/ytrack` for the host's skills root,
|
|
230
|
+
* spelled exactly so (no `..`, no other host's root), and no component from
|
|
231
|
+
* `home` (or, for a root outside home, the root's parent) down to it may be a
|
|
232
|
+
* symlink: its real path must be the anchor's real path plus the same
|
|
233
|
+
* relative path.
|
|
234
|
+
*/
|
|
235
|
+
export function ytrackSkillPathRefusal(
|
|
236
|
+
recorded: string,
|
|
237
|
+
root: string,
|
|
238
|
+
home: string,
|
|
239
|
+
): string | null {
|
|
240
|
+
const expected = path.join(path.resolve(root), YTRACK_SKILL);
|
|
241
|
+
if (recorded !== expected)
|
|
242
|
+
return `${recorded} is not ${expected}, the only ytrack skill directory Workit installs for this host`;
|
|
243
|
+
const inHome = path.relative(path.resolve(home), expected);
|
|
244
|
+
const anchor =
|
|
245
|
+
inHome && !inHome.startsWith("..") && !path.isAbsolute(inHome)
|
|
246
|
+
? path.resolve(home)
|
|
247
|
+
: path.dirname(path.resolve(root));
|
|
248
|
+
const realAnchor = realpathOrNull(anchor);
|
|
249
|
+
if (realAnchor === null) return null; // nothing there to delete
|
|
250
|
+
const lexical = path.join(realAnchor, path.relative(anchor, expected));
|
|
251
|
+
// Walk down from the anchor: an existing component must be itself, not a link.
|
|
252
|
+
let current = realAnchor;
|
|
253
|
+
for (const part of path.relative(anchor, expected).split(path.sep).filter(Boolean)) {
|
|
254
|
+
current = path.join(current, part);
|
|
255
|
+
if (!existsSync(current) && !isLink(current)) return null;
|
|
256
|
+
if (isLink(current) || realpathOrNull(current) !== current)
|
|
257
|
+
return `${current} is a symlink: ${expected} does not resolve to ${lexical}`;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Remove one recorded skill directory: only `<root>/ytrack` for the host's
|
|
264
|
+
* skills root (`ytrackSkillPathRefusal`), a real directory that still holds
|
|
265
|
+
* the ytrack skill; never a symlink, another path or another skill.
|
|
266
|
+
*/
|
|
267
|
+
export function removeYtrackSkill(
|
|
268
|
+
install: YtrackSkillInstall,
|
|
269
|
+
stateDir: string,
|
|
270
|
+
expected: { root: string; home: string },
|
|
271
|
+
): { status: "removed" | "skipped" | "failed"; detail?: string } {
|
|
272
|
+
const forget = () =>
|
|
273
|
+
writeRecord(
|
|
274
|
+
stateDir,
|
|
275
|
+
readYtrackSkillRecord(stateDir).filter(
|
|
276
|
+
(row) => !(row.host === install.host && row.path === install.path),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
const refusal = ytrackSkillPathRefusal(install.path, expected.root, expected.home);
|
|
280
|
+
if (refusal) return { status: "skipped", detail: `left untouched: ${refusal}` };
|
|
281
|
+
if (!existsSync(install.path) && !isLink(install.path)) {
|
|
282
|
+
forget();
|
|
283
|
+
return { status: "skipped", detail: "directory already absent" };
|
|
284
|
+
}
|
|
285
|
+
if (isLink(install.path) || installedSkillVersion(install.path) === null)
|
|
286
|
+
return {
|
|
287
|
+
status: "failed",
|
|
288
|
+
detail: `${install.path} no longer holds the ytrack skill Workit installed; left untouched`,
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
rmSync(install.path, { recursive: true, force: true });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return { status: "failed", detail: error instanceof Error ? error.message : String(error) };
|
|
294
|
+
}
|
|
295
|
+
forget();
|
|
296
|
+
return { status: "removed" };
|
|
297
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
SCHEMA_VERSION,
|
|
7
|
+
POLICY_VERSION,
|
|
8
|
+
OPERATION_FAMILIES,
|
|
9
|
+
operationSchemas,
|
|
10
|
+
advertisedOperationSchemas,
|
|
11
|
+
parseOperation,
|
|
12
|
+
success,
|
|
13
|
+
failure,
|
|
14
|
+
canonicalJson,
|
|
15
|
+
sha256,
|
|
16
|
+
newId,
|
|
17
|
+
newRevision,
|
|
18
|
+
decisionDigest,
|
|
19
|
+
candidateDigest,
|
|
20
|
+
requirementId,
|
|
21
|
+
rewriteRecordRefs,
|
|
22
|
+
} from "./core/task-contract";
|
|
23
|
+
export {
|
|
24
|
+
flatOperationJsonSchema,
|
|
25
|
+
normalizeOperationInput,
|
|
26
|
+
operationDescription,
|
|
27
|
+
operationJsonSchema,
|
|
28
|
+
parseAdvertisedOperation,
|
|
29
|
+
} from "./core/operation-input";
|
|
30
|
+
export type {
|
|
31
|
+
OperationFamily,
|
|
32
|
+
OperationRequest,
|
|
33
|
+
TaskStartRequest,
|
|
34
|
+
Caller,
|
|
35
|
+
Scope,
|
|
36
|
+
Ref,
|
|
37
|
+
Judgment,
|
|
38
|
+
Dimension,
|
|
39
|
+
Requirement,
|
|
40
|
+
Constraint,
|
|
41
|
+
Intent,
|
|
42
|
+
Progress,
|
|
43
|
+
Policy,
|
|
44
|
+
Candidate,
|
|
45
|
+
Capability,
|
|
46
|
+
Entry,
|
|
47
|
+
EvidenceEvaluation,
|
|
48
|
+
Evidence,
|
|
49
|
+
Outcome,
|
|
50
|
+
Decision,
|
|
51
|
+
Finding,
|
|
52
|
+
Assignment,
|
|
53
|
+
WorkerReport,
|
|
54
|
+
Worker,
|
|
55
|
+
TaskRecord,
|
|
56
|
+
WorkspaceRecord,
|
|
57
|
+
TaskView,
|
|
58
|
+
TaskSummary,
|
|
59
|
+
ExportBundle,
|
|
60
|
+
RefMap,
|
|
61
|
+
Result as ContractResult,
|
|
62
|
+
} from "./core/task-contract";
|
|
63
|
+
export { TaskStore, runtimeVersion } from "./core/task-store";
|
|
64
|
+
export type { TaskIndexEntry } from "./core/task-store";
|
|
65
|
+
export { compactTaskContext, reconcileResume } from "./core/task-context";
|
|
66
|
+
export type {
|
|
67
|
+
CompactTaskContext,
|
|
68
|
+
ResumeObservation,
|
|
69
|
+
ResumeObservationInput,
|
|
70
|
+
ResumeReconciliation,
|
|
71
|
+
} from "./core/task-context";
|
|
72
|
+
export { METHODS, invariantBootstrap, selectMethods } from "./core/methods";
|
|
73
|
+
export type { MethodId, SelectedMethod } from "./core/methods";
|
|
74
|
+
export { shellBranchPolicyViolation, shellBranchTarget } from "./core/route-intent";
|
|
75
|
+
export { classifyBranchDirt } from "./core/branch";
|
|
76
|
+
export type { BranchDirt } from "./core/branch";
|
|
77
|
+
export { bundleHashOfFile, isEphemeralCachePath, sha256Hex } from "./core/runtime-identity";
|
|
78
|
+
export {
|
|
79
|
+
captureCandidate,
|
|
80
|
+
checkPin,
|
|
81
|
+
evaluateEvidence,
|
|
82
|
+
evaluateRequirements,
|
|
83
|
+
evaluateClosure,
|
|
84
|
+
findingVerificationPasses,
|
|
85
|
+
resolveBinding,
|
|
86
|
+
} from "./core/task-evaluation";
|
|
87
|
+
export type { CandidateEnvironment, ClosureEvaluation } from "./core/task-evaluation";
|
|
88
|
+
export { WorkitCore } from "./core/task-engine";
|
|
89
|
+
export type { OperationContext } from "./core/task-engine";
|
|
90
|
+
export type {
|
|
91
|
+
HostSession,
|
|
92
|
+
NativeWorkerObservation,
|
|
93
|
+
NativeWorkerVerification,
|
|
94
|
+
NativeWorkerVerifier,
|
|
95
|
+
} from "./core/workers";
|
|
96
|
+
export { contextReadJsonSchema, parseContextRead, readExternalContext } from "./core/context-read";
|
|
97
|
+
export type { ContextReadPayload } from "./core/context-read";
|
|
98
|
+
|
|
99
|
+
export type Result<T> =
|
|
100
|
+
| { ok: true; data: T; error: null }
|
|
101
|
+
| { ok: false; data: T | null; error: string };
|
|
102
|
+
|
|
103
|
+
export const ok = <T>(data: T): Result<T> => ({ ok: true, data, error: null });
|
|
104
|
+
export const fail = <T = never>(error: string, data: T | null = null): Result<T> => ({
|
|
105
|
+
ok: false,
|
|
106
|
+
data,
|
|
107
|
+
error,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const revision = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
|
|
111
|
+
|
|
112
|
+
export function gitRevisionParts(value: string): string[] {
|
|
113
|
+
if (!value || value.startsWith("-") || /[\s\\'"`$;|&<>]/.test(value)) {
|
|
114
|
+
throw new Error("invalid Git revision or range");
|
|
115
|
+
}
|
|
116
|
+
const separator = value.includes("...") ? "..." : value.includes("..") ? ".." : null;
|
|
117
|
+
const parts = separator ? value.split(separator) : [value];
|
|
118
|
+
if (parts.length > 2 || parts.some((part) => !revision.test(part))) {
|
|
119
|
+
throw new Error("invalid Git revision or range");
|
|
120
|
+
}
|
|
121
|
+
return parts;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveGitRevision(root: string, value: string): void {
|
|
125
|
+
for (const part of gitRevisionParts(value)) {
|
|
126
|
+
const result = run(root, "git", [
|
|
127
|
+
"rev-parse",
|
|
128
|
+
"--verify",
|
|
129
|
+
"--quiet",
|
|
130
|
+
"--end-of-options",
|
|
131
|
+
`${part}^{commit}`,
|
|
132
|
+
]);
|
|
133
|
+
if (result.exitCode !== 0) throw new Error(`invalid Git revision or range: ${value}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resolveInside(root: string, candidate: string): string {
|
|
138
|
+
const base = realpathSync(root);
|
|
139
|
+
const target = path.resolve(base, candidate);
|
|
140
|
+
if (target !== base && !target.startsWith(base + path.sep)) {
|
|
141
|
+
throw new Error("path must stay inside repository root");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let ancestor = target;
|
|
145
|
+
while (!existsSync(ancestor)) ancestor = path.dirname(ancestor);
|
|
146
|
+
const canonicalAncestor = realpathSync(ancestor);
|
|
147
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path.sep)) {
|
|
148
|
+
throw new Error("path must stay inside repository root");
|
|
149
|
+
}
|
|
150
|
+
return target;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function run(
|
|
154
|
+
root: string,
|
|
155
|
+
executable: string,
|
|
156
|
+
args: string[],
|
|
157
|
+
env: Record<string, string> = {},
|
|
158
|
+
) {
|
|
159
|
+
const cwd = realpathSync(root);
|
|
160
|
+
const result = spawnSync(executable, args, {
|
|
161
|
+
cwd,
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
env: { ...process.env, ...env },
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
exitCode: result.status ?? 1,
|
|
167
|
+
stdout: result.stdout ?? "",
|
|
168
|
+
stderr: result.stderr ?? result.error?.message ?? "",
|
|
169
|
+
cwd,
|
|
170
|
+
};
|
|
171
|
+
}
|