@open-mercato/cli 0.6.7-develop.6695.1.d1b48a09b3 → 0.6.7-develop.6706.1.b3a4c759bb
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/dist/agentic/guides/module-facts.json +110 -109
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +1 -1
- package/dist/agentic/guides/modules/audit_logs.md +1 -1
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +1 -1
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +1 -1
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +1 -1
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +2 -2
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +1 -1
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +1 -1
- package/dist/agentic/guides/modules/wms.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/guides/testing-debugging.md +6 -0
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/ai/harness/README.md +4 -4
- package/dist/agentic/shared/ai/harness/RELEASE.md +5 -5
- package/dist/agentic/shared/ai/harness/cases.json +174 -1
- package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
- package/dist/agentic/shared/ai/harness/release-matrix.json +5 -4
- package/dist/agentic/shared/ai/harness/validators.json +2 -2
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +23 -20
- package/dist/agentic/shared/ai/review-checklist.md +5 -5
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +2 -2
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +1 -1
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +1 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +3 -1
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +1 -1
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +2 -2
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +66 -1
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
- package/dist/agentic/shared/ai/skills/tiers.json +1 -0
- package/dist/agentic/shared/ai/trackers/github.md +106 -3
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path'
|
|
19
|
+
import { spawnSync } from 'node:child_process'
|
|
20
|
+
import { TextDecoder } from 'node:util'
|
|
21
|
+
|
|
22
|
+
const MAX_SESSION_BYTES = 20 * 1024 * 1024
|
|
23
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024
|
|
24
|
+
const MAX_TOTAL_FILE_BYTES = 20 * 1024 * 1024
|
|
25
|
+
const MAX_PUBLIC_ARTIFACT_BYTES = 25 * 1024 * 1024
|
|
26
|
+
const MAX_ARCHIVE_BYTES = MAX_PUBLIC_ARTIFACT_BYTES
|
|
27
|
+
const MAX_PUBLIC_BUNDLE_BYTES = 30 * 1024 * 1024
|
|
28
|
+
const SHARE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{1,46}[a-z0-9]$/
|
|
29
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true })
|
|
30
|
+
|
|
31
|
+
const sensitiveValueKeyPattern = /^(authorization|cookie|set-cookie|password|passwd|pwd|secret|client[_-]?secret|api[_-]?key|access[_-]?token|refresh[_-]?token|private[_-]?key)$/i
|
|
32
|
+
const pseudonymKeyPattern = /^(session[_-]?id|thread[_-]?id|user[_-]?id|account[_-]?id|organization[_-]?id|tenant[_-]?id|git[_-]?branch)$/i
|
|
33
|
+
const pathKeyPattern = /^(cwd|file|filePath|file_path|filename|path)$/i
|
|
34
|
+
const commandKeyPattern = /^(command|cmd|shell)$/i
|
|
35
|
+
const dangerousPathPattern = /(^|\/)(\.env(?:\.[^/]*)?|\.ssh|\.aws|\.gcloud|\.kube|id_(?:rsa|dsa|ecdsa|ed25519)|credentials(?:\.json)?|service-account(?:\.json)?|\.npmrc|\.pypirc)(\/|$)|\.(?:pem|key|p12|pfx|keystore)$/i
|
|
36
|
+
const completeRedactionMarkerPattern = /^«redacted:[a-z0-9-]+(?::[a-f0-9]{12})?»$/
|
|
37
|
+
|
|
38
|
+
function fail(message) {
|
|
39
|
+
throw new Error(message)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArguments(argv) {
|
|
43
|
+
const allowed = new Set(['name', 'session', 'project-root', 'files', 'out', 'redact-list'])
|
|
44
|
+
const values = new Map()
|
|
45
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
46
|
+
const argument = argv[index]
|
|
47
|
+
if (!argument.startsWith('--')) fail('Every argument must use a named --flag.')
|
|
48
|
+
const key = argument.slice(2)
|
|
49
|
+
if (!allowed.has(key)) fail(`Unknown argument: --${key}`)
|
|
50
|
+
if (values.has(key)) fail(`Duplicate argument: --${key}`)
|
|
51
|
+
const value = argv[index + 1]
|
|
52
|
+
if (!value || value.startsWith('--')) fail(`Missing value for --${key}.`)
|
|
53
|
+
values.set(key, value)
|
|
54
|
+
index += 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const required of ['name', 'session', 'project-root', 'files', 'out']) {
|
|
58
|
+
if (!values.has(required)) fail(`Missing required argument: --${required}.`)
|
|
59
|
+
}
|
|
60
|
+
return Object.fromEntries(values)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sha256(value) {
|
|
64
|
+
return createHash('sha256').update(value).digest('hex')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pseudonym(value, label, salt) {
|
|
68
|
+
const digest = createHash('sha256').update(salt).update('\0').update(value).digest('hex')
|
|
69
|
+
return `«redacted:${label}:${digest.slice(0, 12)}»`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function escapeRegExp(value) {
|
|
73
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeSlashes(value) {
|
|
77
|
+
return value.replaceAll('\\', '/')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isWithin(root, candidate) {
|
|
81
|
+
const pathFromRoot = relative(root, candidate)
|
|
82
|
+
return pathFromRoot !== '' && !pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== '..' && !isAbsolute(pathFromRoot)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isDangerousPath(value) {
|
|
86
|
+
const normalized = value.replaceAll('\\', '/')
|
|
87
|
+
return dangerousPathPattern.test(normalized)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function countReplacement(state, category) {
|
|
91
|
+
state.redactions[category] += 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function replaceMatches(value, pattern, replacement, state, category) {
|
|
95
|
+
return value.replace(pattern, (...match) => {
|
|
96
|
+
countReplacement(state, category)
|
|
97
|
+
return typeof replacement === 'function' ? replacement(...match) : replacement
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shannonEntropy(value) {
|
|
102
|
+
const frequencies = new Map()
|
|
103
|
+
for (const character of value) frequencies.set(character, (frequencies.get(character) ?? 0) + 1)
|
|
104
|
+
let entropy = 0
|
|
105
|
+
for (const count of frequencies.values()) {
|
|
106
|
+
const probability = count / value.length
|
|
107
|
+
entropy -= probability * Math.log2(probability)
|
|
108
|
+
}
|
|
109
|
+
return entropy
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function redactHighEntropy(value, state) {
|
|
113
|
+
return value.replace(/[A-Za-z0-9+/_=-]{32,}/g, (candidate) => {
|
|
114
|
+
if (/^[a-f0-9]+$/i.test(candidate) || /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(candidate)) return candidate
|
|
115
|
+
if (!/[A-Za-z]/.test(candidate) || !/[0-9]/.test(candidate) || shannonEntropy(candidate) < 4.1) return candidate
|
|
116
|
+
countReplacement(state, 'secrets')
|
|
117
|
+
return '«redacted:high-entropy»'
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sanitizeString(input, key, state) {
|
|
122
|
+
if (completeRedactionMarkerPattern.test(input)) return input
|
|
123
|
+
if (sensitiveValueKeyPattern.test(key) && input.length > 0) {
|
|
124
|
+
countReplacement(state, 'secrets')
|
|
125
|
+
return '«redacted:secret-value»'
|
|
126
|
+
}
|
|
127
|
+
if (pseudonymKeyPattern.test(key) && input.length > 0) {
|
|
128
|
+
countReplacement(state, 'identifiers')
|
|
129
|
+
return pseudonym(input, 'identifier', state.pseudonymSalt)
|
|
130
|
+
}
|
|
131
|
+
if (commandKeyPattern.test(key) && isDangerousPath(input)) {
|
|
132
|
+
countReplacement(state, 'dangerous')
|
|
133
|
+
return '«redacted:dangerous-file-command»'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let value = input
|
|
137
|
+
if (state.projectRootPattern) {
|
|
138
|
+
value = replaceMatches(value, state.projectRootPattern, '<project-root>', state, 'paths')
|
|
139
|
+
}
|
|
140
|
+
if (state.homePattern) {
|
|
141
|
+
value = replaceMatches(value, state.homePattern, '~', state, 'paths')
|
|
142
|
+
}
|
|
143
|
+
value = replaceMatches(value, /(?:\/Users|\/home)\/[^/\s"']+/g, '~', state, 'paths')
|
|
144
|
+
value = replaceMatches(value, /[A-Za-z]:\\Users\\[^\\\s"']+/g, '~', state, 'paths')
|
|
145
|
+
|
|
146
|
+
for (const literal of state.customLiterals) {
|
|
147
|
+
const pattern = new RegExp(escapeRegExp(literal), 'g')
|
|
148
|
+
value = replaceMatches(value, pattern, '«redacted:custom-literal»', state, 'custom')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
value = replaceMatches(
|
|
152
|
+
value,
|
|
153
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
154
|
+
'«redacted:private-key»',
|
|
155
|
+
state,
|
|
156
|
+
'secrets',
|
|
157
|
+
)
|
|
158
|
+
value = replaceMatches(value, /\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, '«redacted:credential»', state, 'secrets')
|
|
159
|
+
value = replaceMatches(value, /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '«redacted:jwt»', state, 'secrets')
|
|
160
|
+
value = replaceMatches(value, /\b(Bearer|Basic)\s+[A-Za-z0-9+/_=.-]{8,}\b/gi, (_match, scheme) => `${scheme} «redacted:authorization»`, state, 'secrets')
|
|
161
|
+
value = replaceMatches(value, /\b(Authorization\s*:\s*)[^\s,;]+/gi, (_match, prefix) => `${prefix}«redacted:authorization»`, state, 'secrets')
|
|
162
|
+
value = replaceMatches(value, /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, (_match, prefix) => `${prefix}«redacted:credential»@`, state, 'secrets')
|
|
163
|
+
value = replaceMatches(value, /\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|secret|client[_-]?secret)\s*[:=]\s*)[^\s,;"']+/gi, (_match, prefix) => `${prefix}«redacted:secret»`, state, 'secrets')
|
|
164
|
+
value = redactHighEntropy(value, state)
|
|
165
|
+
|
|
166
|
+
value = replaceMatches(value, /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '«redacted:email»', state, 'pii')
|
|
167
|
+
value = replaceMatches(value, /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '«redacted:ip»', state, 'pii')
|
|
168
|
+
value = replaceMatches(value, /\b(?:[A-F0-9]{1,4}:){2,7}[A-F0-9]{1,4}\b/gi, '«redacted:ip»', state, 'pii')
|
|
169
|
+
value = replaceMatches(value, /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, '«redacted:identifier»', state, 'identifiers')
|
|
170
|
+
value = replaceMatches(value, /\b[0-9a-f]{64,}\b/gi, '«redacted:hex-secret»', state, 'secrets')
|
|
171
|
+
value = value.replace(/\+?\d[\d ()-]{7,}\d/g, (candidate) => {
|
|
172
|
+
const digits = candidate.replace(/\D/g, '')
|
|
173
|
+
if (digits.length < 9 || digits.length > 15) return candidate
|
|
174
|
+
countReplacement(state, 'pii')
|
|
175
|
+
return '«redacted:phone»'
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sanitizeNode(value, key, state) {
|
|
182
|
+
if (typeof value === 'string') return sanitizeString(value, key, state)
|
|
183
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeNode(item, '', state))
|
|
184
|
+
if (!value || typeof value !== 'object') return value
|
|
185
|
+
|
|
186
|
+
for (const [candidateKey, candidateValue] of Object.entries(value)) {
|
|
187
|
+
if (pathKeyPattern.test(candidateKey) && typeof candidateValue === 'string' && isDangerousPath(candidateValue)) {
|
|
188
|
+
countReplacement(state, 'dangerous')
|
|
189
|
+
return { redacted: 'dangerous-file-reference' }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const output = Object.create(null)
|
|
194
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
195
|
+
const sanitizedKey = sanitizeString(childKey, 'object-key', state)
|
|
196
|
+
if (Object.hasOwn(output, sanitizedKey)) fail('Sanitization produced duplicate object keys.')
|
|
197
|
+
output[sanitizedKey] = sanitizeNode(childValue, childKey, state)
|
|
198
|
+
}
|
|
199
|
+
return output
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inferRole(value) {
|
|
203
|
+
if (!value || typeof value !== 'object') return null
|
|
204
|
+
for (const candidate of [value.type, value.role, value.message?.role]) {
|
|
205
|
+
if (candidate === 'user' || candidate === 'assistant') return candidate
|
|
206
|
+
}
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function analyzeSession(session) {
|
|
211
|
+
let entries
|
|
212
|
+
let collectionName
|
|
213
|
+
if (Array.isArray(session)) {
|
|
214
|
+
entries = session
|
|
215
|
+
collectionName = 'root'
|
|
216
|
+
} else if (session && typeof session === 'object') {
|
|
217
|
+
for (const key of ['turns', 'messages', 'events', 'conversation']) {
|
|
218
|
+
if (Array.isArray(session[key])) {
|
|
219
|
+
entries = session[key]
|
|
220
|
+
collectionName = key
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!entries) fail('Session JSON has no recognizable turn/event collection.')
|
|
226
|
+
|
|
227
|
+
const roles = entries.map(inferRole).filter(Boolean)
|
|
228
|
+
const userTurns = roles.filter((role) => role === 'user').length
|
|
229
|
+
const assistantTurns = roles.filter((role) => role === 'assistant').length
|
|
230
|
+
if (userTurns === 0 || assistantTurns === 0) {
|
|
231
|
+
fail('Session JSON must contain at least one recognizable user turn and one assistant turn.')
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
collection: collectionName,
|
|
235
|
+
entries: entries.length,
|
|
236
|
+
recognizedTurns: roles.length,
|
|
237
|
+
userTurns,
|
|
238
|
+
assistantTurns,
|
|
239
|
+
firstRecognizedRole: roles[0],
|
|
240
|
+
lastRecognizedRole: roles.at(-1),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readUtf8File(path, maxBytes, label) {
|
|
245
|
+
const stat = lstatSync(path)
|
|
246
|
+
if (!stat.isFile()) fail(`${label} must be a regular file.`)
|
|
247
|
+
if (stat.isSymbolicLink()) fail(`${label} must not be a symlink.`)
|
|
248
|
+
if (stat.size > maxBytes) fail(`${label} exceeds the allowed size.`)
|
|
249
|
+
const buffer = readFileSync(path)
|
|
250
|
+
if (buffer.includes(0)) fail(`${label} contains binary data.`)
|
|
251
|
+
try {
|
|
252
|
+
return { content: textDecoder.decode(buffer), stat }
|
|
253
|
+
} catch {
|
|
254
|
+
fail(`${label} is not valid UTF-8 text.`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function loadCustomLiterals(path) {
|
|
259
|
+
if (!path) return []
|
|
260
|
+
const { content } = readUtf8File(resolve(path), 1024 * 1024, 'Redaction list')
|
|
261
|
+
const literals = [...new Set(content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean))]
|
|
262
|
+
for (const literal of literals) {
|
|
263
|
+
if (literal.length < 4) fail('Every custom redaction literal must contain at least four characters.')
|
|
264
|
+
}
|
|
265
|
+
return literals.sort((left, right) => right.length - left.length)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function validateRelativePath(value) {
|
|
269
|
+
if (value.includes('\0') || isAbsolute(value)) fail('Generated-file paths must be relative and contain no NUL bytes.')
|
|
270
|
+
const withSlashes = normalizeSlashes(value)
|
|
271
|
+
if (withSlashes.startsWith('../') || withSlashes.includes('/../')) {
|
|
272
|
+
fail('Generated-file paths must stay below the project root.')
|
|
273
|
+
}
|
|
274
|
+
const normalized = posix.normalize(withSlashes.replace(/^\.\//, ''))
|
|
275
|
+
if (!normalized || normalized === '.' || normalized.startsWith('../')) fail('Generated-file paths must stay below the project root.')
|
|
276
|
+
if (isDangerousPath(normalized)) fail(`Dangerous generated-file path rejected: ${normalized}`)
|
|
277
|
+
if (/@|(?:\/Users|\/home)\//i.test(normalized)) fail(`Potentially identifying generated-file path rejected: ${normalized}`)
|
|
278
|
+
return normalized
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function loadGeneratedFiles(manifestPath, projectRoot, state) {
|
|
282
|
+
const { content } = readUtf8File(manifestPath, 1024 * 1024, 'Files manifest')
|
|
283
|
+
const relativePaths = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'))
|
|
284
|
+
if (relativePaths.length === 0) fail('Files manifest must name at least one generated or modified file.')
|
|
285
|
+
if (new Set(relativePaths).size !== relativePaths.length) fail('Files manifest contains duplicate paths.')
|
|
286
|
+
const safePaths = relativePaths.map(validateRelativePath)
|
|
287
|
+
if (new Set(safePaths).size !== safePaths.length) fail('Files manifest contains paths that resolve to the same normalized path.')
|
|
288
|
+
|
|
289
|
+
let totalBytes = 0
|
|
290
|
+
return safePaths.map((safePath) => {
|
|
291
|
+
const resolvedPath = resolve(projectRoot, safePath)
|
|
292
|
+
if (!existsSync(resolvedPath)) fail(`Generated file does not exist: ${safePath}`)
|
|
293
|
+
const stat = lstatSync(resolvedPath)
|
|
294
|
+
if (stat.isSymbolicLink()) fail(`Generated file must not be a symlink: ${safePath}`)
|
|
295
|
+
const realPath = realpathSync(resolvedPath)
|
|
296
|
+
if (!isWithin(projectRoot, realPath)) fail(`Generated file escapes the project root: ${safePath}`)
|
|
297
|
+
const { content: sourceContent } = readUtf8File(realPath, MAX_FILE_BYTES, `Generated file ${safePath}`)
|
|
298
|
+
totalBytes += stat.size
|
|
299
|
+
if (totalBytes > MAX_TOTAL_FILE_BYTES) fail('Generated files exceed the total allowed size.')
|
|
300
|
+
const sanitizedContent = sanitizeString(sourceContent, 'file-content', state)
|
|
301
|
+
return {
|
|
302
|
+
relativePath: safePath,
|
|
303
|
+
mode: stat.mode & 0o777,
|
|
304
|
+
originalBytes: stat.size,
|
|
305
|
+
sanitizedContent,
|
|
306
|
+
sanitizedBytes: Buffer.byteLength(sanitizedContent),
|
|
307
|
+
sha256: sha256(sanitizedContent),
|
|
308
|
+
}
|
|
309
|
+
}).sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function createResidualScanState(state) {
|
|
313
|
+
return {
|
|
314
|
+
customLiterals: state.customLiterals,
|
|
315
|
+
projectRootPattern: state.projectRootPattern,
|
|
316
|
+
homePattern: state.homePattern,
|
|
317
|
+
pseudonymSalt: state.pseudonymSalt,
|
|
318
|
+
redactions: { secrets: 0, pii: 0, paths: 0, dangerous: 0, identifiers: 0, custom: 0 },
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function verifyResidualScan(sanitizedSession, files, state) {
|
|
323
|
+
const scanState = createResidualScanState(state)
|
|
324
|
+
if (JSON.stringify(sanitizeNode(sanitizedSession, '', scanState)) !== JSON.stringify(sanitizedSession)) {
|
|
325
|
+
fail('Residual privacy scan found sensitive session content after sanitization.')
|
|
326
|
+
}
|
|
327
|
+
for (const file of files) {
|
|
328
|
+
if (sanitizeString(file.relativePath, 'generated-file-path', scanState) !== file.relativePath) {
|
|
329
|
+
fail('Residual privacy scan found a sensitive generated-file path after sanitization.')
|
|
330
|
+
}
|
|
331
|
+
if (sanitizeString(file.sanitizedContent, 'file-content', scanState) !== file.sanitizedContent) {
|
|
332
|
+
fail('Residual privacy scan found sensitive generated-file content after sanitization.')
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function runGit(argumentsList, cwd) {
|
|
338
|
+
const result = spawnSync('git', argumentsList, { cwd, encoding: 'utf8' })
|
|
339
|
+
if (result.status !== 0) fail('Git could not create the generated-files ZIP archive.')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function createArchive(files, sourceDirectory, archivePath) {
|
|
343
|
+
for (const file of files) {
|
|
344
|
+
const destination = join(sourceDirectory, ...file.relativePath.split('/'))
|
|
345
|
+
mkdirSync(dirname(destination), { recursive: true })
|
|
346
|
+
writeFileSync(destination, file.sanitizedContent, 'utf8')
|
|
347
|
+
chmodSync(destination, file.mode)
|
|
348
|
+
}
|
|
349
|
+
runGit(['init', '--quiet'], sourceDirectory)
|
|
350
|
+
runGit(['add', '--all'], sourceDirectory)
|
|
351
|
+
runGit(['-c', 'user.name=Session Share', '-c', 'user.email=session-share.invalid', 'commit', '--quiet', '--message', 'session share bundle'], sourceDirectory)
|
|
352
|
+
runGit(['archive', '--format=zip', `--output=${archivePath}`, 'HEAD'], sourceDirectory)
|
|
353
|
+
const archive = readFileSync(archivePath)
|
|
354
|
+
if (archive.length > MAX_ARCHIVE_BYTES) fail('Generated-files ZIP exceeds the allowed size.')
|
|
355
|
+
return archive
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function main() {
|
|
359
|
+
const args = parseArguments(process.argv.slice(2))
|
|
360
|
+
if (!SHARE_NAME_PATTERN.test(args.name)) fail('Share name must be a 3–48 character kebab-case slug.')
|
|
361
|
+
|
|
362
|
+
const sessionPath = resolve(args.session)
|
|
363
|
+
const manifestPath = resolve(args.files)
|
|
364
|
+
const projectRoot = realpathSync(resolve(args['project-root']))
|
|
365
|
+
const outputPath = resolve(args.out)
|
|
366
|
+
if (existsSync(outputPath)) fail('Output path already exists; use a new temporary location.')
|
|
367
|
+
|
|
368
|
+
const outputParent = dirname(outputPath)
|
|
369
|
+
mkdirSync(outputParent, { recursive: true })
|
|
370
|
+
const stagingDirectory = mkdtempSync(join(outputParent, `.${basename(outputPath)}-preparing-`))
|
|
371
|
+
const bundleDirectory = join(stagingDirectory, 'bundle')
|
|
372
|
+
const archiveSourceDirectory = join(stagingDirectory, 'archive-source')
|
|
373
|
+
mkdirSync(bundleDirectory)
|
|
374
|
+
mkdirSync(archiveSourceDirectory)
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const homeDirectory = process.env.HOME ? resolve(process.env.HOME) : null
|
|
378
|
+
const state = {
|
|
379
|
+
customLiterals: loadCustomLiterals(args['redact-list']),
|
|
380
|
+
projectRootPattern: new RegExp(escapeRegExp(projectRoot), 'g'),
|
|
381
|
+
homePattern: homeDirectory ? new RegExp(escapeRegExp(homeDirectory), 'g') : null,
|
|
382
|
+
pseudonymSalt: randomBytes(32),
|
|
383
|
+
redactions: { secrets: 0, pii: 0, paths: 0, dangerous: 0, identifiers: 0, custom: 0 },
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const { content: sessionSource } = readUtf8File(sessionPath, MAX_SESSION_BYTES, 'Session export')
|
|
387
|
+
let session
|
|
388
|
+
try {
|
|
389
|
+
session = JSON.parse(sessionSource)
|
|
390
|
+
} catch {
|
|
391
|
+
fail('Session export is not valid JSON.')
|
|
392
|
+
}
|
|
393
|
+
const sessionSummary = analyzeSession(session)
|
|
394
|
+
const sanitizedSession = sanitizeNode(session, '', state)
|
|
395
|
+
const sanitizedSessionText = `${JSON.stringify(sanitizedSession, null, 2)}\n`
|
|
396
|
+
const sessionOutputPath = join(bundleDirectory, 'session.json')
|
|
397
|
+
writeFileSync(sessionOutputPath, sanitizedSessionText, 'utf8')
|
|
398
|
+
|
|
399
|
+
const files = loadGeneratedFiles(manifestPath, projectRoot, state)
|
|
400
|
+
verifyResidualScan(sanitizedSession, files, state)
|
|
401
|
+
const archivePath = join(bundleDirectory, 'generated-files.zip')
|
|
402
|
+
const archive = createArchive(files, archiveSourceDirectory, archivePath)
|
|
403
|
+
|
|
404
|
+
const reviewDirectory = join(bundleDirectory, 'review', 'generated-files')
|
|
405
|
+
mkdirSync(reviewDirectory, { recursive: true })
|
|
406
|
+
for (const file of files) {
|
|
407
|
+
const source = join(archiveSourceDirectory, ...file.relativePath.split('/'))
|
|
408
|
+
const destination = join(reviewDirectory, ...file.relativePath.split('/'))
|
|
409
|
+
mkdirSync(dirname(destination), { recursive: true })
|
|
410
|
+
copyFileSync(source, destination)
|
|
411
|
+
chmodSync(destination, file.mode)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const privacyReport = {
|
|
415
|
+
version: 1,
|
|
416
|
+
automatedScan: 'pass',
|
|
417
|
+
semanticReview: 'required',
|
|
418
|
+
guarantee: 'Automated scanning cannot prove that contextual personal or confidential data is absent.',
|
|
419
|
+
redactions: state.redactions,
|
|
420
|
+
rejectedInputClasses: ['dangerous paths', 'symlinks', 'binary or invalid UTF-8 files', 'out-of-root files', 'oversized inputs'],
|
|
421
|
+
}
|
|
422
|
+
const privacyReportText = `${JSON.stringify(privacyReport, null, 2)}\n`
|
|
423
|
+
const privacyReportPath = join(bundleDirectory, 'privacy-report.json')
|
|
424
|
+
writeFileSync(privacyReportPath, privacyReportText, 'utf8')
|
|
425
|
+
|
|
426
|
+
const manifest = {
|
|
427
|
+
version: 1,
|
|
428
|
+
shareName: args.name,
|
|
429
|
+
createdAt: new Date().toISOString(),
|
|
430
|
+
session: {
|
|
431
|
+
...sessionSummary,
|
|
432
|
+
bytes: Buffer.byteLength(sanitizedSessionText),
|
|
433
|
+
sha256: sha256(sanitizedSessionText),
|
|
434
|
+
},
|
|
435
|
+
generatedFiles: files.map(({ relativePath, mode, originalBytes, sanitizedBytes, sha256: fileHash }) => ({
|
|
436
|
+
path: relativePath,
|
|
437
|
+
mode: mode.toString(8).padStart(3, '0'),
|
|
438
|
+
originalBytes,
|
|
439
|
+
sanitizedBytes,
|
|
440
|
+
sha256: fileHash,
|
|
441
|
+
})),
|
|
442
|
+
artifacts: {
|
|
443
|
+
'generated-files.zip': { bytes: archive.length, sha256: sha256(archive) },
|
|
444
|
+
'privacy-report.json': { bytes: Buffer.byteLength(privacyReportText), sha256: sha256(privacyReportText) },
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
const manifestText = `${JSON.stringify(manifest, null, 2)}\n`
|
|
448
|
+
const publishedArtifactSizes = [
|
|
449
|
+
Buffer.byteLength(sanitizedSessionText),
|
|
450
|
+
archive.length,
|
|
451
|
+
Buffer.byteLength(manifestText),
|
|
452
|
+
Buffer.byteLength(privacyReportText),
|
|
453
|
+
]
|
|
454
|
+
if (publishedArtifactSizes.some((size) => size > MAX_PUBLIC_ARTIFACT_BYTES)) {
|
|
455
|
+
fail('A public session-share artifact exceeds the provider size limit.')
|
|
456
|
+
}
|
|
457
|
+
if (publishedArtifactSizes.reduce((total, size) => total + size, 0) > MAX_PUBLIC_BUNDLE_BYTES) {
|
|
458
|
+
fail('Public session-share artifacts exceed the provider total size limit.')
|
|
459
|
+
}
|
|
460
|
+
writeFileSync(join(bundleDirectory, 'manifest.json'), manifestText, 'utf8')
|
|
461
|
+
|
|
462
|
+
renameSync(bundleDirectory, outputPath)
|
|
463
|
+
rmSync(stagingDirectory, { recursive: true, force: true })
|
|
464
|
+
process.stdout.write(`${JSON.stringify({
|
|
465
|
+
status: 'prepared',
|
|
466
|
+
entries: sessionSummary.entries,
|
|
467
|
+
userTurns: sessionSummary.userTurns,
|
|
468
|
+
assistantTurns: sessionSummary.assistantTurns,
|
|
469
|
+
generatedFiles: files.length,
|
|
470
|
+
archiveBytes: archive.length,
|
|
471
|
+
redactions: state.redactions,
|
|
472
|
+
semanticReview: 'required',
|
|
473
|
+
})}\n`)
|
|
474
|
+
} catch (error) {
|
|
475
|
+
rmSync(stagingDirectory, { recursive: true, force: true })
|
|
476
|
+
throw error
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
main()
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const message = error instanceof Error ? error.message : 'Unknown bundle-preparation failure.'
|
|
484
|
+
process.stderr.write(`Session share bundle was not prepared: ${message}\n`)
|
|
485
|
+
process.exitCode = 1
|
|
486
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tracker provider: GitHub
|
|
2
2
|
|
|
3
|
-
This file is the GitHub implementation of the tracker operations contract (see `TEMPLATE.md` for the contract itself).
|
|
3
|
+
This file is the GitHub implementation of the tracker operations contract (see `TEMPLATE.md` for the contract itself). Skills perform issue/PR state management through **named tracker operations** — `**get-issue**`, `**comment-pr**`, and so on — and this file defines what each operation means for GitHub, using the `gh` CLI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
At runtime: `om-setup-agent-pipeline` copies this file into the repository at `.ai/trackers/github.md`, and the config's `tracker` field selects it. When a skill says "tracker operation **get-pr**", execute the command documented under that operation heading in the repo's copy. The repo's copy is authoritative: teams extend or override any operation by editing it, and every skill picks the change up on its next run. An operation not covered by an edit keeps its behavior from this file's text as copied.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
@@ -98,6 +98,84 @@ BASE_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
|
|
98
98
|
[ -z "$BASE_BRANCH" ] && BASE_BRANCH="main"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Public session-share artifacts
|
|
102
|
+
|
|
103
|
+
#### publish-session-share
|
|
104
|
+
`{owner}/{repo}`, `{branch}`, `{shareName}`, and a local `{bundleDir}` → atomically publish the four reviewed session-share artifacts on a new public branch and return repository, branch, commit, branch URL, and raw artifact URLs as JSON.
|
|
105
|
+
|
|
106
|
+
This operation is intentionally strict: the repository must already be public; the branch must be a new slash-free `session-share-…` ref; and the bundle directory must contain regular, non-symlinked `session.json`, `generated-files.zip`, `manifest.json`, and `privacy-report.json` files within the documented size cap. Git blobs, a tree, and a commit are created first; the public ref is created last, so a preparation failure does not expose a partial branch.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
TARGET_REPO="{owner}/{repo}"
|
|
110
|
+
SHARE_BRANCH="{branch}"
|
|
111
|
+
SHARE_NAME="{shareName}"
|
|
112
|
+
BUNDLE_DIR="{bundleDir}"
|
|
113
|
+
printf '%s' "$TARGET_REPO" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' || exit 1
|
|
114
|
+
printf '%s' "$SHARE_NAME" | grep -Eq '^[a-z0-9][a-z0-9-]{1,46}[a-z0-9]$' || exit 1
|
|
115
|
+
[ "$SHARE_BRANCH" = "session-share-$SHARE_NAME" ] || exit 1
|
|
116
|
+
[ "$(gh repo view "$TARGET_REPO" --json visibility --jq .visibility)" = "PUBLIC" ] || exit 1
|
|
117
|
+
gh api "repos/${TARGET_REPO}/git/ref/heads/${SHARE_BRANCH}" >/dev/null 2>&1 && exit 1
|
|
118
|
+
|
|
119
|
+
SESSION_SHARE_TMP=$(mktemp -d)
|
|
120
|
+
trap 'rm -rf "$SESSION_SHARE_TMP"' EXIT HUP INT TERM
|
|
121
|
+
printf '[]\n' > "$SESSION_SHARE_TMP/tree.json"
|
|
122
|
+
TOTAL_BYTES=0
|
|
123
|
+
for ARTIFACT in session.json generated-files.zip manifest.json privacy-report.json; do
|
|
124
|
+
ARTIFACT_PATH="$BUNDLE_DIR/$ARTIFACT"
|
|
125
|
+
[ -f "$ARTIFACT_PATH" ] && [ ! -L "$ARTIFACT_PATH" ] || exit 1
|
|
126
|
+
ARTIFACT_BYTES=$(wc -c < "$ARTIFACT_PATH" | tr -d ' ')
|
|
127
|
+
TOTAL_BYTES=$((TOTAL_BYTES + ARTIFACT_BYTES))
|
|
128
|
+
[ "$ARTIFACT_BYTES" -le 26214400 ] && [ "$TOTAL_BYTES" -le 31457280 ] || exit 1
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
for ARTIFACT in session.json generated-files.zip manifest.json privacy-report.json; do
|
|
132
|
+
ARTIFACT_PATH="$BUNDLE_DIR/$ARTIFACT"
|
|
133
|
+
base64 < "$ARTIFACT_PATH" | tr -d '\n' > "$SESSION_SHARE_TMP/content.b64"
|
|
134
|
+
BLOB_SHA=$(jq -n --rawfile content "$SESSION_SHARE_TMP/content.b64" '{content:$content,encoding:"base64"}' \
|
|
135
|
+
| gh api -X POST "repos/${TARGET_REPO}/git/blobs" --input - --jq .sha) || exit 1
|
|
136
|
+
jq --arg path "$ARTIFACT" --arg sha "$BLOB_SHA" \
|
|
137
|
+
'. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' \
|
|
138
|
+
"$SESSION_SHARE_TMP/tree.json" > "$SESSION_SHARE_TMP/tree.next.json" || exit 1
|
|
139
|
+
mv "$SESSION_SHARE_TMP/tree.next.json" "$SESSION_SHARE_TMP/tree.json"
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
DEFAULT_BRANCH=$(gh repo view "$TARGET_REPO" --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
|
|
143
|
+
BASE_COMMIT=$(gh api "repos/${TARGET_REPO}/git/ref/heads/${DEFAULT_BRANCH}" --jq .object.sha) || exit 1
|
|
144
|
+
BASE_TREE=$(gh api "repos/${TARGET_REPO}/git/commits/${BASE_COMMIT}" --jq .tree.sha) || exit 1
|
|
145
|
+
TREE_SHA=$(jq -n --arg base "$BASE_TREE" --slurpfile entries "$SESSION_SHARE_TMP/tree.json" \
|
|
146
|
+
'{base_tree:$base,tree:$entries[0]}' \
|
|
147
|
+
| gh api -X POST "repos/${TARGET_REPO}/git/trees" --input - --jq .sha) || exit 1
|
|
148
|
+
COMMIT_SHA=$(jq -n --arg message "session share ${SHARE_NAME}" --arg tree "$TREE_SHA" --arg parent "$BASE_COMMIT" \
|
|
149
|
+
'{message:$message,tree:$tree,parents:[$parent]}' \
|
|
150
|
+
| gh api -X POST "repos/${TARGET_REPO}/git/commits" --input - --jq .sha) || exit 1
|
|
151
|
+
jq -n --arg ref "refs/heads/${SHARE_BRANCH}" --arg sha "$COMMIT_SHA" '{ref:$ref,sha:$sha}' \
|
|
152
|
+
| gh api -X POST "repos/${TARGET_REPO}/git/refs" --input - >/dev/null || exit 1
|
|
153
|
+
|
|
154
|
+
jq -n \
|
|
155
|
+
--arg repository "$TARGET_REPO" \
|
|
156
|
+
--arg branch "$SHARE_BRANCH" \
|
|
157
|
+
--arg commit "$COMMIT_SHA" \
|
|
158
|
+
--arg branchUrl "https://github.com/${TARGET_REPO}/tree/${SHARE_BRANCH}" \
|
|
159
|
+
--arg sessionUrl "https://raw.githubusercontent.com/${TARGET_REPO}/${SHARE_BRANCH}/session.json" \
|
|
160
|
+
--arg archiveUrl "https://raw.githubusercontent.com/${TARGET_REPO}/${SHARE_BRANCH}/generated-files.zip" \
|
|
161
|
+
--arg manifestUrl "https://raw.githubusercontent.com/${TARGET_REPO}/${SHARE_BRANCH}/manifest.json" \
|
|
162
|
+
--arg privacyUrl "https://raw.githubusercontent.com/${TARGET_REPO}/${SHARE_BRANCH}/privacy-report.json" \
|
|
163
|
+
'{repository:$repository,branch:$branch,commit:$commit,branchUrl:$branchUrl,artifacts:{session:$sessionUrl,archive:$archiveUrl,manifest:$manifestUrl,privacy:$privacyUrl}}'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### delete-session-share
|
|
167
|
+
`{owner}/{repo}`, `{branch}`, and `{shareName}` → remove only the exact derived session-share ref after a failed issue creation or an explicit retention cleanup request. Deleting a public ref cannot guarantee erasure from clones, forks, caches, logs, or archives.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
TARGET_REPO="{owner}/{repo}"
|
|
171
|
+
SHARE_BRANCH="{branch}"
|
|
172
|
+
SHARE_NAME="{shareName}"
|
|
173
|
+
printf '%s' "$TARGET_REPO" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' || exit 1
|
|
174
|
+
printf '%s' "$SHARE_NAME" | grep -Eq '^[a-z0-9][a-z0-9-]{1,46}[a-z0-9]$' || exit 1
|
|
175
|
+
[ "$SHARE_BRANCH" = "session-share-$SHARE_NAME" ] || exit 1
|
|
176
|
+
gh api -X DELETE "repos/${TARGET_REPO}/git/refs/heads/${SHARE_BRANCH}"
|
|
177
|
+
```
|
|
178
|
+
|
|
101
179
|
### Issues
|
|
102
180
|
|
|
103
181
|
#### get-issue
|
|
@@ -109,7 +187,9 @@ gh issue view {issueId} --repo {owner}/{repo} --json number,title,body,state,aut
|
|
|
109
187
|
#### search-issues
|
|
110
188
|
Query (text, `in:title,body`, state) → matching issues.
|
|
111
189
|
```bash
|
|
112
|
-
|
|
190
|
+
ISSUE_STATE="{state}"
|
|
191
|
+
case "$ISSUE_STATE" in open|closed|all) ;; *) exit 1 ;; esac
|
|
192
|
+
gh issue list --repo {owner}/{repo} --state "$ISSUE_STATE" --search "<query> in:title,body" --json number,title,url
|
|
113
193
|
```
|
|
114
194
|
|
|
115
195
|
#### create-issue
|
|
@@ -130,6 +210,12 @@ gh issue close {issueId} --repo {owner}/{repo} --reason completed --comment "<co
|
|
|
130
210
|
gh issue comment {issueId} --repo {owner}/{repo} --body "<body>"
|
|
131
211
|
```
|
|
132
212
|
|
|
213
|
+
#### update-issue
|
|
214
|
+
`{issueId}`, new title and/or body (use a body-file for multi-line bodies). Edits the issue's own fields; does not touch labels or assignees (those have their own operations).
|
|
215
|
+
```bash
|
|
216
|
+
gh issue edit {issueId} --repo {owner}/{repo} --title "<title>" --body-file <file>
|
|
217
|
+
```
|
|
218
|
+
|
|
133
219
|
#### assign-issue / unassign-issue
|
|
134
220
|
```bash
|
|
135
221
|
gh issue edit {issueId} --repo {owner}/{repo} --add-assignee "<login>"
|
|
@@ -151,6 +237,12 @@ gh api repos/{owner}/{repo}/issues/comments/{commentId} --jq '{body,user:.user.l
|
|
|
151
237
|
gh api repos/{owner}/{repo}/issues/{number}/comments --jq '.[] | {id,user:.user.login,body}'
|
|
152
238
|
```
|
|
153
239
|
|
|
240
|
+
#### update-comment
|
|
241
|
+
`{commentId}`, new body → rewrite an existing conversation comment in place (works for issue and PR conversation comments alike). This is how marker-idempotent comments (label rationale, verification, claim take-overs) are updated on re-runs: find your `🤖 …` marker via **list-issue-comments**, then update that comment instead of posting a new one. Use a body file so multi-line bodies survive.
|
|
242
|
+
```bash
|
|
243
|
+
gh api -X PATCH repos/{owner}/{repo}/issues/comments/{commentId} -F body=@<path>
|
|
244
|
+
```
|
|
245
|
+
|
|
154
246
|
### Pull requests
|
|
155
247
|
|
|
156
248
|
#### get-pr
|
|
@@ -181,6 +273,17 @@ PR_URL=$(gh pr view --json url --jq .url)
|
|
|
181
273
|
PR_NUMBER=$(gh pr view --json number --jq .number)
|
|
182
274
|
```
|
|
183
275
|
|
|
276
|
+
#### update-pr
|
|
277
|
+
`{prNumber}`, new title and/or new body → the PR's own title/body rewritten in place (not a comment), e.g. reframing a doc-originated spec PR that grew a feature implementation. Pass whichever of `--title` / `--body-file` changed; omit the other. Use a body file so multi-line bodies survive.
|
|
278
|
+
```bash
|
|
279
|
+
gh pr edit {prNumber} --title "<title>"
|
|
280
|
+
gh pr edit {prNumber} --body-file <path-or-process-substitution>
|
|
281
|
+
```
|
|
282
|
+
If `gh pr edit` silently no-ops in this repo (some GitHub setups do), fall back to the REST API:
|
|
283
|
+
```bash
|
|
284
|
+
gh api -X PATCH repos/{owner}/{repo}/pulls/{prNumber} -f title="<title>" -f body="$(cat <path>)"
|
|
285
|
+
```
|
|
286
|
+
|
|
184
287
|
#### comment-pr
|
|
185
288
|
`{prNumber}`, body. For long structured comments:
|
|
186
289
|
```bash
|
|
@@ -47,7 +47,7 @@ Usage:
|
|
|
47
47
|
Options:
|
|
48
48
|
--root <absolute-app> Controller/generated app root (default: current directory)
|
|
49
49
|
--runner <codex|claude> Required primary runner for every blocking live lane
|
|
50
|
-
--portability-runner <runner> Optional different runner for the
|
|
50
|
+
--portability-runner <runner> Optional different runner for the 46-case read-only portability lane
|
|
51
51
|
--prepare-targets <absolute> Clone this fresh scaffold once per writable case under an empty/new directory
|
|
52
52
|
--writable-targets <absolute> JSON map of every writable case to a fresh disposable app
|
|
53
53
|
--case-timeout <ms> Per-model invocation timeout floor (default: 120000; a writable case may raise it)
|