@plainconceptsplatform/agent-harness 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +454 -454
  3. package/cli/commands/join.js +259 -259
  4. package/cli/commands/migrate.js +310 -310
  5. package/cli/commands/shared.js +27 -27
  6. package/cli/commands/single.js +79 -79
  7. package/cli/commands/wizard.js +134 -134
  8. package/cli/fragments/ops-backlog/az.md +28 -28
  9. package/cli/fragments/ops-backlog/gh.md +28 -28
  10. package/cli/fragments/ops-backlog/jira.md +28 -28
  11. package/cli/fragments/ops-review/az.md +62 -62
  12. package/cli/fragments/ops-review/gh.md +51 -51
  13. package/cli/fragments/ops-review/gl.md +55 -55
  14. package/cli/fragments/ops-ship/gh.md +67 -67
  15. package/cli/fragments/ops-ship/gl.md +84 -84
  16. package/cli/index.js +116 -116
  17. package/cli/presets/clean.json +21 -21
  18. package/cli/presets/models.json +68 -68
  19. package/cli/presets/optimization.json +32 -32
  20. package/cli/presets/platforms.json +76 -76
  21. package/cli/presets/source.json +23 -23
  22. package/cli/steps/clean/index.js +120 -120
  23. package/cli/steps/copy/agents.js +120 -120
  24. package/cli/steps/copy/fullstack-engineer.js +89 -89
  25. package/cli/steps/copy/opencode-json.js +324 -324
  26. package/cli/steps/copy/skills.js +316 -307
  27. package/cli/steps/models/format.js +88 -88
  28. package/cli/steps/models/index.js +64 -64
  29. package/cli/steps/openspec/index.js +136 -136
  30. package/cli/steps/optimization/codegraph.js +141 -141
  31. package/cli/steps/optimization/detect.js +66 -66
  32. package/cli/steps/optimization/humanizer.js +17 -17
  33. package/cli/steps/optimization/index.js +158 -158
  34. package/cli/steps/optimization/memory.js +91 -91
  35. package/cli/steps/optimization/patch-guardrails.js +111 -111
  36. package/cli/steps/optimization/simple-english.js +17 -17
  37. package/cli/steps/optimization/skills-lock.js +30 -30
  38. package/cli/steps/platform/index.js +109 -109
  39. package/cli/steps/source/index.js +123 -123
  40. package/cli/utils/agent-color.js +111 -111
  41. package/cli/utils/copy.js +134 -134
  42. package/cli/utils/exec-spinner.js +47 -47
  43. package/cli/utils/exec.js +134 -134
  44. package/cli/utils/legacy-check.js +30 -30
  45. package/cli/utils/models-cache.js +58 -58
  46. package/cli/utils/models-pricing.js +42 -42
  47. package/cli/utils/paths.js +81 -80
  48. package/cli/utils/process.js +3 -3
  49. package/cli/utils/terminal.js +6 -6
  50. package/cli/utils/update-manifest.js +75 -75
  51. package/harness/.opencode/_gitignore +10 -9
  52. package/harness/.opencode/commands/ops-backlog.md +10 -10
  53. package/harness/.opencode/commands/ops-review.md +10 -8
  54. package/harness/.opencode/commands/ops-ship.md +9 -9
  55. package/harness/.opencode/commands/plan-archive.md +7 -5
  56. package/harness/.opencode/commands/plan-explore.md +11 -9
  57. package/harness/.opencode/commands/plan-propose.md +11 -9
  58. package/harness/.opencode/commands/plan-quick.md +7 -5
  59. package/harness/.opencode/commands/plan-story.md +11 -9
  60. package/harness/.opencode/commands/repo-audit.md +7 -5
  61. package/harness/.opencode/commands/repo-help.md +5 -5
  62. package/harness/.opencode/package.json +5 -5
  63. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  64. package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -354
  65. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  66. package/harness/.opencode/scripts/check-diff.mjs +244 -244
  67. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  68. package/harness/ARCHITECTURE.md +13 -13
  69. package/harness/DESIGN.md +13 -13
  70. package/harness/opencode.jsonc +36 -36
  71. package/harness/openspec/config.yaml +20 -20
  72. package/harness/skills-lock.json +17 -17
  73. package/package.json +12 -14
  74. package/skills/agent-harness-cli/SKILL.md +86 -86
  75. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  76. package/skills/pc-guardrails-project/SKILL.md +8 -8
  77. package/skills/pc-make-architecture/structure-template.md +64 -64
  78. package/skills/pc-make-engineer/SKILL.md +59 -59
  79. package/skills/pc-make-engineer/template.md +36 -36
  80. package/skills/pc-make-guardrails/category-reference.md +112 -112
  81. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  82. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  83. package/skills/pc-ops-ship/SKILL.md +22 -22
  84. package/skills/pc-plan-apply/SKILL.md +86 -86
  85. package/skills/pc-plan-apply/simple-mode.md +21 -21
  86. package/skills/pc-plan-explore/SKILL.md +21 -21
  87. package/skills/pc-plan-goal/SKILL.md +91 -91
  88. package/skills/pc-plan-goal/failure-policy.md +30 -30
  89. package/skills/pc-plan-goal/output-mode.md +10 -10
  90. package/skills/pc-plan-propose/SKILL.md +125 -125
  91. package/skills/pc-plan-propose/task-annotation.md +39 -39
  92. package/skills/pc-plan-quick/SKILL.md +46 -46
  93. package/skills/pc-repo-audit/SKILL.md +44 -44
  94. package/skills/pc-repo-help/SKILL.md +87 -87
  95. package/skills/pc-repo-verify/SKILL.md +150 -150
@@ -1,30 +1,30 @@
1
- import fse from 'fs-extra'
2
- import path from 'node:path'
3
- import { CONFIG_FILE, LEGACY_CONFIG_FILES, OPENCODE_DIR } from './paths.js'
4
-
5
- /**
6
- * Detect a repo onboarded by opencode-onboard v1.x.
7
- *
8
- * v2 renamed both the state files and the `ob-` skill prefix, and it does not
9
- * migrate: see the rename plan. Running against a v1 repo would otherwise fail
10
- * silently in two places, because `patchFile` returns without logging when its
11
- * markers are absent and the skill sync only removes stale directories under
12
- * `forceOverwrite && !updateMode`. The result is a half-patched harness that
13
- * still reports success, so refuse the run instead.
14
- */
15
- export async function findLegacyInstall(cwd = process.cwd()) {
16
- const opencodeDir = path.join(cwd, OPENCODE_DIR)
17
- if (await fse.pathExists(path.join(opencodeDir, CONFIG_FILE))) return null
18
-
19
- const found = []
20
- for (const file of LEGACY_CONFIG_FILES) {
21
- if (await fse.pathExists(path.join(opencodeDir, file))) found.push(`${OPENCODE_DIR}/${file}`)
22
- }
23
-
24
- const skillsDir = path.join(cwd, '.agents', 'skills')
25
- const legacySkills = (await fse.readdir(skillsDir).catch(() => []))
26
- .filter(entry => entry.startsWith('ob-'))
27
-
28
- if (found.length === 0 && legacySkills.length === 0) return null
29
- return { files: found, skills: legacySkills }
30
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'node:path'
3
+ import { CONFIG_FILE, LEGACY_CONFIG_FILES, OPENCODE_DIR } from './paths.js'
4
+
5
+ /**
6
+ * Detect a repo onboarded by opencode-onboard v1.x.
7
+ *
8
+ * v2 renamed both the state files and the `ob-` skill prefix, and it does not
9
+ * migrate: see the rename plan. Running against a v1 repo would otherwise fail
10
+ * silently in two places, because `patchFile` returns without logging when its
11
+ * markers are absent and the skill sync only removes stale directories under
12
+ * `forceOverwrite && !updateMode`. The result is a half-patched harness that
13
+ * still reports success, so refuse the run instead.
14
+ */
15
+ export async function findLegacyInstall(cwd = process.cwd()) {
16
+ const opencodeDir = path.join(cwd, OPENCODE_DIR)
17
+ if (await fse.pathExists(path.join(opencodeDir, CONFIG_FILE))) return null
18
+
19
+ const found = []
20
+ for (const file of LEGACY_CONFIG_FILES) {
21
+ if (await fse.pathExists(path.join(opencodeDir, file))) found.push(`${OPENCODE_DIR}/${file}`)
22
+ }
23
+
24
+ const skillsDir = path.join(cwd, '.agents', 'skills')
25
+ const legacySkills = (await fse.readdir(skillsDir).catch(() => []))
26
+ .filter(entry => entry.startsWith('ob-'))
27
+
28
+ if (found.length === 0 && legacySkills.length === 0) return null
29
+ return { files: found, skills: legacySkills }
30
+ }
@@ -1,58 +1,58 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { parseModels } from './models-pricing.js'
4
- import { modelsCacheDir } from './paths.js'
5
-
6
- const CACHE_DIR = modelsCacheDir();
7
- const CACHE_FILE = path.join(CACHE_DIR, 'models-cache.json');
8
- const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
9
- const MODELS_URL = 'https://models.dev/api.json';
10
-
11
- async function loadCache() {
12
- try {
13
- if (!await fse.pathExists(CACHE_FILE)) return null;
14
- const cache = await fse.readJson(CACHE_FILE);
15
- if (!cache.timestamp || !cache.models) return null;
16
- const age = Date.now() - cache.timestamp;
17
- if (age > CACHE_TTL_MS) return null; // expired
18
- return cache.models;
19
- } catch {
20
- return null;
21
- }
22
- }
23
-
24
- async function saveCache(models) {
25
- try {
26
- await fse.ensureDir(CACHE_DIR);
27
- await fse.writeJson(CACHE_FILE, { timestamp: Date.now(), models });
28
- } catch {
29
- // cache write failure is non-fatal
30
- }
31
- }
32
-
33
- export async function fetchModels() {
34
- // 1. Try cache first (fresh)
35
- const cached = await loadCache();
36
- if (cached) return { models: cached, source: 'cache' };
37
-
38
- // 2. Try network
39
- try {
40
- const res = await fetch(MODELS_URL, { signal: AbortSignal.timeout(8000) });
41
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
42
- const data = await res.json();
43
- const models = parseModels(data);
44
- await saveCache(models);
45
- return { models, source: 'network' };
46
- } catch {
47
- // 3. Network failed, fall back to stale cache if available
48
- try {
49
- if (await fse.pathExists(CACHE_FILE)) {
50
- const cache = await fse.readJson(CACHE_FILE);
51
- if (cache.models?.length) return { models: cache.models, source: 'stale-cache' };
52
- }
53
- } catch {
54
- // ignore
55
- }
56
- return { models: null, source: 'unavailable' };
57
- }
58
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { parseModels } from './models-pricing.js'
4
+ import { modelsCacheDir } from './paths.js'
5
+
6
+ const CACHE_DIR = modelsCacheDir();
7
+ const CACHE_FILE = path.join(CACHE_DIR, 'models-cache.json');
8
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
9
+ const MODELS_URL = 'https://models.dev/api.json';
10
+
11
+ async function loadCache() {
12
+ try {
13
+ if (!await fse.pathExists(CACHE_FILE)) return null;
14
+ const cache = await fse.readJson(CACHE_FILE);
15
+ if (!cache.timestamp || !cache.models) return null;
16
+ const age = Date.now() - cache.timestamp;
17
+ if (age > CACHE_TTL_MS) return null; // expired
18
+ return cache.models;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ async function saveCache(models) {
25
+ try {
26
+ await fse.ensureDir(CACHE_DIR);
27
+ await fse.writeJson(CACHE_FILE, { timestamp: Date.now(), models });
28
+ } catch {
29
+ // cache write failure is non-fatal
30
+ }
31
+ }
32
+
33
+ export async function fetchModels() {
34
+ // 1. Try cache first (fresh)
35
+ const cached = await loadCache();
36
+ if (cached) return { models: cached, source: 'cache' };
37
+
38
+ // 2. Try network
39
+ try {
40
+ const res = await fetch(MODELS_URL, { signal: AbortSignal.timeout(8000) });
41
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
42
+ const data = await res.json();
43
+ const models = parseModels(data);
44
+ await saveCache(models);
45
+ return { models, source: 'network' };
46
+ } catch {
47
+ // 3. Network failed, fall back to stale cache if available
48
+ try {
49
+ if (await fse.pathExists(CACHE_FILE)) {
50
+ const cache = await fse.readJson(CACHE_FILE);
51
+ if (cache.models?.length) return { models: cache.models, source: 'stale-cache' };
52
+ }
53
+ } catch {
54
+ // ignore
55
+ }
56
+ return { models: null, source: 'unavailable' };
57
+ }
58
+ }
@@ -1,42 +1,42 @@
1
- // Providers considered "canonical" for reference pricing, in priority order.
2
- // When a model's own provider has no cost (e.g. github-copilot shows $0),
3
- // we look up the same model name in these providers and attach canonicalCost.
4
- export const CANONICAL_PROVIDERS = ['anthropic', 'openai', 'google', 'mistral', 'meta', 'cohere'];
5
-
6
- export function parseModels(data) {
7
- // Build name → canonical cost lookup from authoritative providers first
8
- const canonicalCostByName = new Map();
9
- for (const providerId of CANONICAL_PROVIDERS) {
10
- const provider = data[providerId];
11
- if (!provider?.models) continue;
12
- for (const model of Object.values(provider.models)) {
13
- if (!model.tool_call) continue;
14
- const name = model.name;
15
- if (name && model.cost?.input !== undefined && !canonicalCostByName.has(name)) {
16
- canonicalCostByName.set(name, model.cost.input);
17
- }
18
- }
19
- }
20
-
21
- const models = [];
22
- for (const [providerId, provider] of Object.entries(data)) {
23
- if (!provider.models) continue;
24
- for (const [modelId, model] of Object.entries(provider.models)) {
25
- if (!model.tool_call) continue;
26
- const name = model.name || modelId;
27
- const cost = model.cost?.input;
28
- const canonicalCost = canonicalCostByName.get(name);
29
- models.push({
30
- id: `${providerId}/${modelId}`,
31
- name,
32
- cost,
33
- // canonicalCost: cost from the authoritative provider for this model name.
34
- // Defined when cost !== canonicalCost (different provider, reseller, or $0 subscription).
35
- canonicalCost: canonicalCost !== undefined && canonicalCost !== cost ? canonicalCost : undefined,
36
- context: model.limit?.context,
37
- });
38
- }
39
- }
40
- models.sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity));
41
- return models;
42
- }
1
+ // Providers considered "canonical" for reference pricing, in priority order.
2
+ // When a model's own provider has no cost (e.g. github-copilot shows $0),
3
+ // we look up the same model name in these providers and attach canonicalCost.
4
+ export const CANONICAL_PROVIDERS = ['anthropic', 'openai', 'google', 'mistral', 'meta', 'cohere'];
5
+
6
+ export function parseModels(data) {
7
+ // Build name → canonical cost lookup from authoritative providers first
8
+ const canonicalCostByName = new Map();
9
+ for (const providerId of CANONICAL_PROVIDERS) {
10
+ const provider = data[providerId];
11
+ if (!provider?.models) continue;
12
+ for (const model of Object.values(provider.models)) {
13
+ if (!model.tool_call) continue;
14
+ const name = model.name;
15
+ if (name && model.cost?.input !== undefined && !canonicalCostByName.has(name)) {
16
+ canonicalCostByName.set(name, model.cost.input);
17
+ }
18
+ }
19
+ }
20
+
21
+ const models = [];
22
+ for (const [providerId, provider] of Object.entries(data)) {
23
+ if (!provider.models) continue;
24
+ for (const [modelId, model] of Object.entries(provider.models)) {
25
+ if (!model.tool_call) continue;
26
+ const name = model.name || modelId;
27
+ const cost = model.cost?.input;
28
+ const canonicalCost = canonicalCostByName.get(name);
29
+ models.push({
30
+ id: `${providerId}/${modelId}`,
31
+ name,
32
+ cost,
33
+ // canonicalCost: cost from the authoritative provider for this model name.
34
+ // Defined when cost !== canonicalCost (different provider, reseller, or $0 subscription).
35
+ canonicalCost: canonicalCost !== undefined && canonicalCost !== cost ? canonicalCost : undefined,
36
+ context: model.limit?.context,
37
+ });
38
+ }
39
+ }
40
+ models.sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity));
41
+ return models;
42
+ }
@@ -1,80 +1,81 @@
1
- import os from 'node:os'
2
- import path from 'node:path'
3
-
4
- /**
5
- * Every harness path that lands in a consumer repo, in one place.
6
- *
7
- * These filenames are a contract with every repo the harness has been
8
- * installed into: the CLI writes them, and the opencode plugins shipped in
9
- * harness/.opencode/plugins/ read them back at runtime. Changing a name
10
- * here means changing it in that payload too, or the plugins go blind.
11
- *
12
- * They are deliberately named for the harness rather than for the CLI, so a
13
- * future rename of the package does not become a rename of the on-disk state.
14
- */
15
- export const OPENCODE_DIR = '.opencode'
16
-
17
- /** Project config written by the metadata step. */
18
- export const CONFIG_FILE = 'harness.json'
19
-
20
- /** Per-developer model override, gitignored, never committed. */
21
- export const USER_CONFIG_FILE = 'harness.user.json'
22
-
23
- /** Hashes of every managed file, so `update` can detect local edits. */
24
- export const MANIFEST_FILE = 'harness-managed.json'
25
-
26
- /** Live subagent wave state, owned by the pc-subagent-monitor plugin. */
27
- export const RUN_STATE_FILE = 'harness-run.json'
28
-
29
- /** Names `.opencode/.gitignore` must cover. Mirrored in content/.opencode/_gitignore. */
30
- export const IGNORED_ENTRIES = [
31
- 'node_modules',
32
- RUN_STATE_FILE,
33
- USER_CONFIG_FILE,
34
- MANIFEST_FILE,
35
- 'source-roots.json',
36
- '*-engineer.*.md',
37
- // Regenerated from fullstack-engineer.md by pc-subagent-tiers every startup.
38
- 'agents/build.md',
39
- 'agents/plan.md',
40
- ]
41
-
42
- /**
43
- * Files the harness shipped and has since retired. `update` deletes them, because a skill or
44
- * command left behind keeps being offered to agents long after the procedure it describes is
45
- * gone. pc-ops-evidence wrote a capturePlan for a CI workflow that no longer exists; the
46
- * verification plan from pc-repo-verify is what the evidence workflow replays now.
47
- */
48
- export const RETIRED_PATHS = [
49
- '.agents/skills/pc-ops-evidence',
50
- '.agents/skills/pc-make-evidence-scaffold',
51
- '.opencode/commands/ops-evidence.md',
52
- '.opencode/commands/make-evidence-scaffold.md',
53
- ]
54
-
55
- export function configPath(cwd = process.cwd()) {
56
- return path.join(cwd, OPENCODE_DIR, CONFIG_FILE)
57
- }
58
-
59
- export function userConfigPath(cwd = process.cwd()) {
60
- return path.join(cwd, OPENCODE_DIR, USER_CONFIG_FILE)
61
- }
62
-
63
- export function manifestPath(cwd = process.cwd()) {
64
- return path.join(cwd, OPENCODE_DIR, MANIFEST_FILE)
65
- }
66
-
67
- /** Cross-run models cache, keyed to the CLI rather than to any one repo. */
68
- export function modelsCacheDir() {
69
- return path.join(os.homedir(), '.config', 'agent-harness')
70
- }
71
-
72
- /**
73
- * Filenames written by opencode-onboard v1.x. Only used to detect a v1 repo
74
- * and refuse to half-patch it: nothing reads their contents.
75
- */
76
- export const LEGACY_CONFIG_FILES = [
77
- 'opencode-onboard.json',
78
- 'opencode-onboard.user.json',
79
- 'opencode-onboard-managed.json',
80
- ]
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * Every harness path that lands in a consumer repo, in one place.
6
+ *
7
+ * These filenames are a contract with every repo the harness has been
8
+ * installed into: the CLI writes them, and the opencode plugins shipped in
9
+ * harness/.opencode/plugins/ read them back at runtime. Changing a name
10
+ * here means changing it in that payload too, or the plugins go blind.
11
+ *
12
+ * They are deliberately named for the harness rather than for the CLI, so a
13
+ * future rename of the package does not become a rename of the on-disk state.
14
+ */
15
+ export const OPENCODE_DIR = '.opencode'
16
+
17
+ /** Project config written by the metadata step. */
18
+ export const CONFIG_FILE = 'harness.json'
19
+
20
+ /** Per-developer model override, gitignored, never committed. */
21
+ export const USER_CONFIG_FILE = 'harness.user.json'
22
+
23
+ /** Hashes of every managed file, so `update` can detect local edits. */
24
+ export const MANIFEST_FILE = 'harness-managed.json'
25
+
26
+ /** Live subagent wave state, owned by the pc-subagent-monitor plugin. */
27
+ export const RUN_STATE_FILE = 'harness-run.json'
28
+
29
+ /** Names `.opencode/.gitignore` must cover. Mirrored in content/.opencode/_gitignore. */
30
+ export const IGNORED_ENTRIES = [
31
+ 'node_modules',
32
+ RUN_STATE_FILE,
33
+ USER_CONFIG_FILE,
34
+ MANIFEST_FILE,
35
+ 'source-roots.json',
36
+ '*-engineer.*.md',
37
+ // Regenerated from fullstack-engineer.md by pc-subagent-tiers every startup.
38
+ 'agents/build.md',
39
+ 'agents/plan.md',
40
+ 'agents/planner.md',
41
+ ]
42
+
43
+ /**
44
+ * Files the harness shipped and has since retired. `update` deletes them, because a skill or
45
+ * command left behind keeps being offered to agents long after the procedure it describes is
46
+ * gone. pc-ops-evidence wrote a capturePlan for a CI workflow that no longer exists; the
47
+ * verification plan from pc-repo-verify is what the evidence workflow replays now.
48
+ */
49
+ export const RETIRED_PATHS = [
50
+ '.agents/skills/pc-ops-evidence',
51
+ '.agents/skills/pc-make-evidence-scaffold',
52
+ '.opencode/commands/ops-evidence.md',
53
+ '.opencode/commands/make-evidence-scaffold.md',
54
+ ]
55
+
56
+ export function configPath(cwd = process.cwd()) {
57
+ return path.join(cwd, OPENCODE_DIR, CONFIG_FILE)
58
+ }
59
+
60
+ export function userConfigPath(cwd = process.cwd()) {
61
+ return path.join(cwd, OPENCODE_DIR, USER_CONFIG_FILE)
62
+ }
63
+
64
+ export function manifestPath(cwd = process.cwd()) {
65
+ return path.join(cwd, OPENCODE_DIR, MANIFEST_FILE)
66
+ }
67
+
68
+ /** Cross-run models cache, keyed to the CLI rather than to any one repo. */
69
+ export function modelsCacheDir() {
70
+ return path.join(os.homedir(), '.config', 'agent-harness')
71
+ }
72
+
73
+ /**
74
+ * Filenames written by opencode-onboard v1.x. Only used to detect a v1 repo
75
+ * and refuse to half-patch it: nothing reads their contents.
76
+ */
77
+ export const LEGACY_CONFIG_FILES = [
78
+ 'opencode-onboard.json',
79
+ 'opencode-onboard.user.json',
80
+ 'opencode-onboard-managed.json',
81
+ ]
@@ -1,3 +1,3 @@
1
- export function exit(code = 0) {
2
- process.exit(code)
3
- }
1
+ export function exit(code = 0) {
2
+ process.exit(code)
3
+ }
@@ -1,6 +1,6 @@
1
- export const MARKERS = {
2
- EMPTY: " ",
3
- OK_PREFIX: "✓ ",
4
- WARN_PREFIX: "⚠ ",
5
- ERROR_PREFIX: "✗ ",
6
- };
1
+ export const MARKERS = {
2
+ EMPTY: " ",
3
+ OK_PREFIX: "✓ ",
4
+ WARN_PREFIX: "⚠ ",
5
+ ERROR_PREFIX: "✗ ",
6
+ };
@@ -1,75 +1,75 @@
1
- import crypto from 'node:crypto'
2
- import fse from 'fs-extra'
3
- import path from 'node:path'
4
- import { MANIFEST_FILE, OPENCODE_DIR } from './paths.js'
5
-
6
- const MANIFEST_RELATIVE_PATH = path.join(OPENCODE_DIR, MANIFEST_FILE)
7
-
8
- function normalizeRelativePath(relativePath) {
9
- return relativePath.split(path.sep).join('/')
10
- }
11
-
12
- function hashContent(content) {
13
- return crypto.createHash('sha256').update(content).digest('hex')
14
- }
15
-
16
- export async function hashFile(filePath) {
17
- return hashContent(await fse.readFile(filePath))
18
- }
19
-
20
- // A file counts as untouched when it differs from what we shipped only in ways
21
- // the harness itself caused. Two of those, and both made the raw-bytes
22
- // comparison call an untouched file modified — permanently, because the flag is
23
- // re-derived on every update:
24
- //
25
- // 1. Marker pairs. The patchers rewrite them after the copy, so a file with
26
- // any PC-* marker never matches its own source again.
27
- // 2. Line endings. A file that reaches a consumer's tree through git arrives
28
- // CRLF on Windows while the shipped source is LF.
29
- //
30
- // This is the general form of the bug that left five repos on a stale
31
- // browser-automation skill through repeated updates.
32
- const MARKER_PAIR = /(<!-- PC-[A-Z0-9-]+-START -->)[\s\S]*?(<!-- PC-[A-Z0-9-]+-END -->)/g
33
-
34
- function comparableContent(buffer) {
35
- return buffer.toString('utf-8').replace(/\r\n/g, '\n').replace(MARKER_PAIR, '$1$2')
36
- }
37
-
38
- export async function hashComparableFile(filePath) {
39
- return hashContent(comparableContent(await fse.readFile(filePath)))
40
- }
41
-
42
- export async function readUpdateManifest(cwd = process.cwd()) {
43
- const manifestPath = path.join(cwd, MANIFEST_RELATIVE_PATH)
44
- const manifest = await fse.readJson(manifestPath).catch(() => ({ version: 1, files: {} }))
45
- if (!manifest.files || typeof manifest.files !== 'object') manifest.files = {}
46
- return manifest
47
- }
48
-
49
- export async function writeUpdateManifest(manifest, cwd = process.cwd()) {
50
- const manifestPath = path.join(cwd, MANIFEST_RELATIVE_PATH)
51
- await fse.ensureDir(path.dirname(manifestPath))
52
- await fse.writeJson(manifestPath, manifest, { spaces: 2 })
53
- }
54
-
55
- export async function canUpdateManagedFile(relativePath, cwd, manifest) {
56
- const normalizedPath = normalizeRelativePath(relativePath)
57
- const destinationPath = path.join(cwd, relativePath)
58
- if (!await fse.pathExists(destinationPath)) return true
59
- const previousHash = manifest.files?.[normalizedPath]
60
- if (!previousHash) return false
61
- // Raw is accepted too: manifests written before this comparison existed hold
62
- // raw source hashes, and re-recording them all would need an update to run
63
- // first, which is the thing being unblocked.
64
- return previousHash === await hashComparableFile(destinationPath)
65
- || previousHash === await hashFile(destinationPath)
66
- }
67
-
68
- export async function recordManagedFile(manifest, relativePath, sourcePath) {
69
- const files = manifest.files
70
- files[normalizeRelativePath(relativePath)] = await hashComparableFile(sourcePath)
71
- }
72
-
73
- export function manifestPath() {
74
- return MANIFEST_RELATIVE_PATH
75
- }
1
+ import crypto from 'node:crypto'
2
+ import fse from 'fs-extra'
3
+ import path from 'node:path'
4
+ import { MANIFEST_FILE, OPENCODE_DIR } from './paths.js'
5
+
6
+ const MANIFEST_RELATIVE_PATH = path.join(OPENCODE_DIR, MANIFEST_FILE)
7
+
8
+ function normalizeRelativePath(relativePath) {
9
+ return relativePath.split(path.sep).join('/')
10
+ }
11
+
12
+ function hashContent(content) {
13
+ return crypto.createHash('sha256').update(content).digest('hex')
14
+ }
15
+
16
+ export async function hashFile(filePath) {
17
+ return hashContent(await fse.readFile(filePath))
18
+ }
19
+
20
+ // A file counts as untouched when it differs from what we shipped only in ways
21
+ // the harness itself caused. Two of those, and both made the raw-bytes
22
+ // comparison call an untouched file modified — permanently, because the flag is
23
+ // re-derived on every update:
24
+ //
25
+ // 1. Marker pairs. The patchers rewrite them after the copy, so a file with
26
+ // any PC-* marker never matches its own source again.
27
+ // 2. Line endings. A file that reaches a consumer's tree through git arrives
28
+ // CRLF on Windows while the shipped source is LF.
29
+ //
30
+ // This is the general form of the bug that left five repos on a stale
31
+ // browser-automation skill through repeated updates.
32
+ const MARKER_PAIR = /(<!-- PC-[A-Z0-9-]+-START -->)[\s\S]*?(<!-- PC-[A-Z0-9-]+-END -->)/g
33
+
34
+ function comparableContent(buffer) {
35
+ return buffer.toString('utf-8').replace(/\r\n/g, '\n').replace(MARKER_PAIR, '$1$2')
36
+ }
37
+
38
+ export async function hashComparableFile(filePath) {
39
+ return hashContent(comparableContent(await fse.readFile(filePath)))
40
+ }
41
+
42
+ export async function readUpdateManifest(cwd = process.cwd()) {
43
+ const manifestPath = path.join(cwd, MANIFEST_RELATIVE_PATH)
44
+ const manifest = await fse.readJson(manifestPath).catch(() => ({ version: 1, files: {} }))
45
+ if (!manifest.files || typeof manifest.files !== 'object') manifest.files = {}
46
+ return manifest
47
+ }
48
+
49
+ export async function writeUpdateManifest(manifest, cwd = process.cwd()) {
50
+ const manifestPath = path.join(cwd, MANIFEST_RELATIVE_PATH)
51
+ await fse.ensureDir(path.dirname(manifestPath))
52
+ await fse.writeJson(manifestPath, manifest, { spaces: 2 })
53
+ }
54
+
55
+ export async function canUpdateManagedFile(relativePath, cwd, manifest) {
56
+ const normalizedPath = normalizeRelativePath(relativePath)
57
+ const destinationPath = path.join(cwd, relativePath)
58
+ if (!await fse.pathExists(destinationPath)) return true
59
+ const previousHash = manifest.files?.[normalizedPath]
60
+ if (!previousHash) return false
61
+ // Raw is accepted too: manifests written before this comparison existed hold
62
+ // raw source hashes, and re-recording them all would need an update to run
63
+ // first, which is the thing being unblocked.
64
+ return previousHash === await hashComparableFile(destinationPath)
65
+ || previousHash === await hashFile(destinationPath)
66
+ }
67
+
68
+ export async function recordManagedFile(manifest, relativePath, sourcePath) {
69
+ const files = manifest.files
70
+ files[normalizeRelativePath(relativePath)] = await hashComparableFile(sourcePath)
71
+ }
72
+
73
+ export function manifestPath() {
74
+ return MANIFEST_RELATIVE_PATH
75
+ }
@@ -1,9 +1,10 @@
1
- node_modules
2
- harness-run.json
3
- harness.user.json
4
- harness-managed.json
5
- source-roots.json
6
- tmp/
7
- *-engineer.*.md
8
- agents/build.md
9
- agents/plan.md
1
+ node_modules
2
+ harness-run.json
3
+ harness.user.json
4
+ harness-managed.json
5
+ source-roots.json
6
+ tmp/
7
+ *-engineer.*.md
8
+ agents/build.md
9
+ agents/plan.md
10
+ agents/planner.md
@@ -1,10 +1,10 @@
1
- ---
2
- description: Create an issue in the backlog platform (GitHub, Azure DevOps, Jira) from a description.
3
- ---
4
-
5
- Backlog platform is set in `.opencode/harness.json` → `platform.backlog`. The platform-specific content below is injected by the CLI during onboarding.
6
-
7
- Input: `$ARGUMENTS` (the issue title/description)
8
-
9
- <!-- PC-PLATFORM-BACKLOG-START -->
10
- <!-- PC-PLATFORM-BACKLOG-END -->
1
+ ---
2
+ description: Create an issue in the backlog platform (GitHub, Azure DevOps, Jira) from a description.
3
+ ---
4
+
5
+ Backlog platform is set in `.opencode/harness.json` → `platform.backlog`. The platform-specific content below is injected by the CLI during onboarding.
6
+
7
+ Input: `$ARGUMENTS` (the issue title/description)
8
+
9
+ <!-- PC-PLATFORM-BACKLOG-START -->
10
+ <!-- PC-PLATFORM-BACKLOG-END -->