@plainconceptsplatform/agent-harness 3.8.1 → 3.8.2

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.
@@ -2,7 +2,7 @@ import fse from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { info, success, warn } from '../../utils/exec.js'
5
- import { ContractError, loadContract } from '../../../harness/.opencode/scripts/platform-contract.mjs'
5
+ import { ContractError, isTemplateSource, loadContract } from '../../../harness/.opencode/scripts/platform-contract.mjs'
6
6
 
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
8
  const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
@@ -74,7 +74,7 @@ function replaceBetween(content, start, end, replacement) {
74
74
  return content.replace(pattern, () => (body ? `${start}\n${body}\n${end}` : `${start}\n${end}`))
75
75
  }
76
76
 
77
- export function reuseBlock(contract) {
77
+ export function reuseBlock(contract, templateSource = false) {
78
78
  const { docs = [], packages = [], names = [] } = contract?.reuse ?? {}
79
79
  if (!docs.length && !packages.length && !names.length) return ''
80
80
  const code = list => list.map(item => `\`${item}\``).join(', ')
@@ -86,13 +86,14 @@ export function reuseBlock(contract) {
86
86
  ...(docs.length ? [`- Never design something new before reading ${code(docs)}.`] : []),
87
87
  ...(packages.length ? [`- Never re-implement an export of ${code(packages)}; import it.`] : []),
88
88
  ...(names.length ? [`- Never write your own ${code(names)}; reuse the existing one.`] : []),
89
- `- Never edit a path listed under \`owned\`. Ask for the change at ${contract.source.changes}, or add a dated exception to \`${contract.exceptions}\`.`,
89
+ ...(templateSource ? [] : [`- Never edit a path listed under \`owned\`. Ask for the change at ${contract.source.changes}, or add a dated exception to \`${contract.exceptions}\`.`]),
90
90
  ].join('\n')
91
91
  }
92
92
 
93
93
  function contractBlock(cwd) {
94
94
  try {
95
- return reuseBlock(loadContract(cwd))
95
+ const contract = loadContract(cwd)
96
+ return reuseBlock(contract, isTemplateSource(cwd, contract))
96
97
  } catch (err) {
97
98
  if (!(err instanceof ContractError)) throw err
98
99
  warn(`${err.message} The "Reuse before you build" block in AGENTS.md was left as it was.`)
@@ -18,7 +18,7 @@ import { execFile } from "node:child_process"
18
18
  import fs from "node:fs/promises"
19
19
  import path from "node:path"
20
20
  import { promisify } from "node:util"
21
- import { ContractError, describeOwner, isOwned, loadContract } from "../scripts/platform-contract.mjs"
21
+ import { ContractError, describeOwner, isOwned, isTemplateSource, loadContract } from "../scripts/platform-contract.mjs"
22
22
 
23
23
  const GUARDRAILS_SKILL = "pc-guardrails-generic"
24
24
  const TIER_SUFFIX = /\.(?:build|fast|plan)$/
@@ -210,7 +210,7 @@ async function checkPlatform(event, root) {
210
210
  return deny(event, error.message, "Fix that file before editing anything else: until it reads, nothing can tell which files the template owns.")
211
211
  }
212
212
  const owned = contract ? event.resources.filter(target => isOwnedTarget(target, root, contract)) : []
213
- if (!owned.length || await syncInProgress(root, contract.sync.marker)) return
213
+ if (!owned.length || isTemplateSource(root, contract) || await syncInProgress(root, contract.sync.marker)) return
214
214
  const { owner, fix } = describeOwner(contract)
215
215
  return deny(event, `\`${owned[0]}\` is ${owner}.`, fix)
216
216
  }
@@ -3,7 +3,7 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { pathToFileURL } from 'node:url'
5
5
  import { commentBlocks, countLines, isGenerated, kindOf, lineKinds, slopFindings, testStats } from './code-rules.mjs'
6
- import { ContractError, describeOwner, isOwned, loadContract, nameKey, reusableNames } from './platform-contract.mjs'
6
+ import { ContractError, describeOwner, isOwned, isTemplateSource, loadContract, nameKey, reusableNames } from './platform-contract.mjs'
7
7
 
8
8
  export const DEFAULT_LIMITS = {
9
9
  commentRatio: 0.1,
@@ -213,6 +213,7 @@ export function trackedFiles(root, baseline) {
213
213
 
214
214
  // A diff that changes the sync marker is a template sync, the one change allowed to rewrite owned files.
215
215
  export function platformViolations(root, base, paths, contract) {
216
+ if (isTemplateSource(root, contract)) return []
216
217
  const touched = new Set(untrackedFiles(root))
217
218
  for (const { status, from, to } of changes(root, base)) {
218
219
  touched.add(to)
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from 'node:child_process'
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
 
@@ -268,6 +269,32 @@ export function loadContract(root) {
268
269
  return { ...contract, rules: toRules(contract.owned), exceptionList: Array.isArray(entries) ? entries.filter(isMapping) : [] }
269
270
  }
270
271
 
272
+ export function normalizeRepoUrl(url) {
273
+ return String(url ?? '')
274
+ .trim()
275
+ .replace(/^git\+/, '')
276
+ .replace(/^gh:/, 'https://github.com/')
277
+ .replace(/^ssh:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\//, 'https://$1/')
278
+ .replace(/^git@([^:]+):/, 'https://$1/')
279
+ .replace(/^(https?:\/\/)[^@/]+@/, '$1')
280
+ .replace(/^http:\/\//, 'https://')
281
+ .replace(/\/+$/, '')
282
+ .replace(/\.git$/, '')
283
+ .toLowerCase()
284
+ }
285
+
286
+ // The template repository carries its own contract, and its owned paths are its own to change.
287
+ export function isTemplateSource(root, contract) {
288
+ if (!contract?.source.url) return false
289
+ let origin
290
+ try {
291
+ origin = execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
292
+ } catch {
293
+ return false
294
+ }
295
+ return normalizeRepoUrl(origin) !== '' && normalizeRepoUrl(origin) === normalizeRepoUrl(contract.source.url)
296
+ }
297
+
271
298
  const localDate = (now = new Date()) =>
272
299
  `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
273
300
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "Installs the Plain Concepts Platform Harness into any codebase, and keeps it up to date. Wires OpenCode, OpenSpec, codegraph, and agentmemory into a multi-agent workflow that runs on native parallel subagents.",
5
5
  "keywords": [
6
6
  "opencode",