@plainconceptsplatform/agent-harness 3.1.1 → 3.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/LICENSE +21 -21
- package/README.md +24 -4
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +120 -120
- package/cli/steps/copy/fullstack-engineer.js +89 -89
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -127
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -88
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -131
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +8 -8
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +5 -5
- package/harness/.opencode/commands/plan-explore.md +9 -9
- package/harness/.opencode/commands/plan-propose.md +9 -9
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/package.json +5 -5
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers.js +354 -354
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
- package/harness/.opencode/scripts/check-diff.mjs +244 -0
- package/harness/.opencode/scripts/code-rules.mjs +288 -0
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/pc-guardrails-generic/SKILL.md +2 -1
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-ops-ship/SKILL.md +22 -18
- package/skills/pc-plan-apply/SKILL.md +86 -86
- package/skills/pc-plan-apply/simple-mode.md +1 -1
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-propose/SKILL.md +125 -125
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +1 -1
- package/skills/pc-repo-audit/SKILL.md +44 -44
|
@@ -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
|
+
}
|
package/cli/utils/process.js
CHANGED
|
@@ -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
|
+
}
|
package/cli/utils/terminal.js
CHANGED
|
@@ -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,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 -->
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Read and triage PR review feedback. Reports what needs fixing, does not implement fixes.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Repo platform is set in `.opencode/harness.json` → `platform.repo`. The platform-specific content below is injected by the CLI during onboarding.
|
|
6
|
-
|
|
7
|
-
<!-- PC-PLATFORM-REVIEW-START -->
|
|
8
|
-
<!-- PC-PLATFORM-REVIEW-END -->
|
|
1
|
+
---
|
|
2
|
+
description: Read and triage PR review feedback. Reports what needs fixing, does not implement fixes.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Repo platform is set in `.opencode/harness.json` → `platform.repo`. The platform-specific content below is injected by the CLI during onboarding.
|
|
6
|
+
|
|
7
|
+
<!-- PC-PLATFORM-REVIEW-START -->
|
|
8
|
+
<!-- PC-PLATFORM-REVIEW-END -->
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Create a pull request for the current feature branch with screenshots if UI changed.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the `pc-ops-ship` skill and execute it.
|
|
6
|
-
|
|
7
|
-
Input:
|
|
8
|
-
|
|
9
|
-
$ARGUMENTS
|
|
1
|
+
---
|
|
2
|
+
description: Create a pull request for the current feature branch with screenshots if UI changed.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `pc-ops-ship` skill and execute it.
|
|
6
|
+
|
|
7
|
+
Input:
|
|
8
|
+
|
|
9
|
+
$ARGUMENTS
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Archive the oldest merged unarchived OpenSpec change and update documentation.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the `pc-plan-archive` skill and execute it in **interactive mode**. No input required.
|
|
1
|
+
---
|
|
2
|
+
description: Archive the oldest merged unarchived OpenSpec change and update documentation.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `pc-plan-archive` skill and execute it in **interactive mode**. No input required.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Think through an idea, investigate a problem, or clarify requirements before creating a change.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the `pc-plan-explore` skill and execute it in **interactive mode**.
|
|
6
|
-
|
|
7
|
-
Input:
|
|
8
|
-
|
|
9
|
-
$ARGUMENTS
|
|
1
|
+
---
|
|
2
|
+
description: Think through an idea, investigate a problem, or clarify requirements before creating a change.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `pc-plan-explore` skill and execute it in **interactive mode**.
|
|
6
|
+
|
|
7
|
+
Input:
|
|
8
|
+
|
|
9
|
+
$ARGUMENTS
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Parse a work item or idea and propose a change plan with enriched task assignments.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the `pc-plan-propose` skill and execute it in **interactive mode**.
|
|
6
|
-
|
|
7
|
-
Input:
|
|
8
|
-
|
|
9
|
-
$ARGUMENTS
|
|
1
|
+
---
|
|
2
|
+
description: Parse a work item or idea and propose a change plan with enriched task assignments.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `pc-plan-propose` skill and execute it in **interactive mode**.
|
|
6
|
+
|
|
7
|
+
Input:
|
|
8
|
+
|
|
9
|
+
$ARGUMENTS
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Show all available commands and when to use each one.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Load the `pc-repo-help` skill and display its reference to the user exactly as written. Do not summarize.
|
|
1
|
+
---
|
|
2
|
+
description: Show all available commands and when to use each one.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Load the `pc-repo-help` skill and display its reference to the user exactly as written. Do not summarize.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
"@opencode/plugin": "2.0.22"
|
|
4
|
-
}
|
|
5
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@opencode/plugin": "2.0.22"
|
|
4
|
+
}
|
|
5
|
+
}
|