@inneranimalmedia/agentsam-sdk 2.4.1 → 2.5.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.
Files changed (158) hide show
  1. package/README.md +7 -6
  2. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  3. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  4. package/docs/CAPABILITIES.md +5 -3
  5. package/docs/CLI_SHELL.md +57 -80
  6. package/docs/CMS_STUDIO.md +101 -0
  7. package/docs/CONTEXT.md +170 -0
  8. package/docs/LOCAL_OLLAMA.md +58 -0
  9. package/docs/MERKLE.md +19 -19
  10. package/docs/PORTABLE_CONTEXT.md +4 -3
  11. package/docs/PROJECT_CONFIG.md +72 -0
  12. package/docs/RELEASES.md +26 -2
  13. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  14. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  15. package/docs/SDK_WORKER.md +86 -0
  16. package/docs/SECURITY.md +59 -22
  17. package/docs/client-cms-editor.md +15 -0
  18. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  19. package/package.json +17 -4
  20. package/packages/identity/package.json +11 -2
  21. package/packages/identity/src/contracts/auth-config.js +98 -0
  22. package/packages/identity/src/index.js +1 -0
  23. package/packages/identity/src/oauth/README.md +2 -2
  24. package/packages/identity/src/oauth/credentials.js +11 -4
  25. package/packages/identity/src/oauth/iam-platform.js +3 -3
  26. package/packages/identity/src/providers/iam/index.js +7 -7
  27. package/packages/identity/src/providers/iam/oauth.js +6 -4
  28. package/packages/identity/src/providers/iam/profile.js +5 -5
  29. package/packages/identity/tests/auth-config.test.mjs +57 -0
  30. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  31. package/protocol/FILEMETA_V1.md +95 -0
  32. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  33. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  34. package/protocol/MERKLE_V1.md +3 -1
  35. package/protocol/capabilities/manifest.json +32 -3
  36. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  37. package/protocol/context/context-budget.schema.json +41 -0
  38. package/protocol/context/context-item.schema.json +20 -0
  39. package/protocol/context/resolved-context-pack.schema.json +42 -0
  40. package/protocol/context/result-policy.schema.json +17 -0
  41. package/protocol/knowledge/context-pack.schema.json +33 -0
  42. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  43. package/python/README.md +15 -7
  44. package/python/agentsam_sdk/cli.py +0 -21
  45. package/python/agentsam_sdk/tui/README.md +17 -12
  46. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  47. package/python/agentsam_sdk/tui/demo.py +25 -10
  48. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  49. package/python/tests/test_tui_cli.py +7 -6
  50. package/skills/README.md +21 -0
  51. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  52. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  53. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  54. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  55. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  56. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  57. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  58. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  59. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  60. package/skills/catalog.json +53 -0
  61. package/src/capabilities/index.js +7 -0
  62. package/src/capabilities/repository-snapshot-view.js +238 -0
  63. package/src/capabilities/repository-snapshot.js +28 -14
  64. package/src/cli.js +108 -56
  65. package/src/commands/context.js +14 -2
  66. package/src/commands/db.js +4 -7
  67. package/src/commands/deploy.js +17 -31
  68. package/src/commands/interactive.js +21 -0
  69. package/src/commands/knowledge.js +27 -4
  70. package/src/commands/merkle-persist.js +118 -0
  71. package/src/commands/merkle.js +32 -17
  72. package/src/commands/models.js +107 -0
  73. package/src/commands/ollama.js +259 -0
  74. package/src/commands/preferences.js +102 -0
  75. package/src/commands/product.js +86 -16
  76. package/src/commands/security.js +3 -3
  77. package/src/commands/shell.js +71 -27
  78. package/src/commands/skills.js +66 -0
  79. package/src/commands/start-local.js +1 -1
  80. package/src/commands/tunnel.js +5 -4
  81. package/src/context/budget.js +58 -0
  82. package/src/context/compact.js +28 -0
  83. package/src/context/index.js +5 -0
  84. package/src/context/resolve.js +84 -0
  85. package/src/context/result-policy.js +66 -0
  86. package/src/index.js +16 -0
  87. package/src/indexing/execution-boundary.js +144 -0
  88. package/src/indexing/index.js +8 -0
  89. package/src/indexing/provider.js +41 -0
  90. package/src/knowledge/config.js +2 -2
  91. package/src/knowledge/context-pack.js +12 -1
  92. package/src/knowledge/contracts.js +9 -4
  93. package/src/knowledge/engine.js +7 -3
  94. package/src/knowledge/service/server.js +1 -1
  95. package/src/lib/bridge-client.js +7 -5
  96. package/src/lib/cli-preferences.js +74 -0
  97. package/src/lib/core-client.js +8 -8
  98. package/src/lib/deploy-receipt/index.js +5 -2
  99. package/src/lib/detect-context.js +6 -5
  100. package/src/lib/identity-scaffold.js +1 -1
  101. package/src/lib/local-scaffold.js +35 -33
  102. package/src/lib/local-status.js +9 -17
  103. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  104. package/src/lib/merkle/filemeta.js +43 -0
  105. package/src/lib/merkle/git-ignore.js +24 -0
  106. package/src/lib/merkle/hash.js +1 -0
  107. package/src/lib/merkle/index.js +22 -0
  108. package/src/lib/merkle/persistence.js +72 -0
  109. package/src/lib/merkle/semantic.js +359 -0
  110. package/src/lib/merkle/snapshot.js +9 -3
  111. package/src/lib/merkle/tree.js +11 -6
  112. package/src/lib/project-config.js +227 -0
  113. package/src/lib/project-rules.js +68 -0
  114. package/src/lib/save-sdk-token.js +1 -1
  115. package/src/lib/slash-commands.js +2 -1
  116. package/src/lib/tools.js +11 -5
  117. package/src/security/index.js +1 -0
  118. package/src/security/inventory.js +4 -1
  119. package/src/security/render.js +27 -5
  120. package/src/security/scan.js +24 -9
  121. package/src/security/trust-boundary.js +24 -0
  122. package/src/skills/index.js +64 -0
  123. package/src/tools/index.js +1 -0
  124. package/src/tools/search.js +70 -0
  125. package/src/ui/ansi.js +1 -1
  126. package/src/ui/boot.js +56 -0
  127. package/src/ui/merkle/render.js +1 -0
  128. package/src/ui/runtime-activity.js +192 -0
  129. package/src/ui/theme.js +19 -18
  130. package/test/app-building-skills.test.mjs +61 -0
  131. package/test/apps-scaffold-contract.test.mjs +56 -0
  132. package/test/capabilities.test.mjs +53 -4
  133. package/test/cli-preferences.test.mjs +25 -0
  134. package/test/context.test.mjs +95 -0
  135. package/test/indexing-provider.test.mjs +29 -0
  136. package/test/jr-dev-skill.test.mjs +26 -0
  137. package/test/knowledge-context-pack.test.mjs +19 -0
  138. package/test/knowledge.test.mjs +1 -1
  139. package/test/merkle-persistence.test.mjs +91 -0
  140. package/test/merkle.test.mjs +77 -3
  141. package/test/models.test.mjs +37 -0
  142. package/test/ollama.test.mjs +94 -0
  143. package/test/project-config.test.mjs +81 -0
  144. package/test/project-rules.test.mjs +44 -0
  145. package/test/release-hygiene.test.mjs +34 -0
  146. package/test/repository-snapshot-view.test.mjs +112 -0
  147. package/test/runtime-activity.test.mjs +98 -0
  148. package/test/sdk-worker-contract.test.mjs +68 -0
  149. package/test/security.test.mjs +46 -0
  150. package/test/shell.test.mjs +8 -1
  151. package/test/skills.test.mjs +22 -0
  152. package/test/smoke.mjs +2 -2
  153. package/test/theme-portability.test.mjs +14 -0
  154. package/test/tools-search.test.mjs +27 -0
  155. package/examples/agentsam-tui-ansi.mjs +0 -149
  156. package/src/commands/tui.js +0 -120
  157. package/src/ui/splash-xterm.js +0 -290
  158. package/src/ui/splash.js +0 -426
@@ -0,0 +1,96 @@
1
+ # Hooks and Operational I/O
2
+
3
+ ## A hook is a boundary-triggered program
4
+
5
+ Examples include Git hooks, CI jobs, deploy hooks, webhooks, queue consumers,
6
+ scheduled tasks, post-migration checks, and runtime alerts. The triggering mechanism
7
+ changes; the engineering questions do not.
8
+
9
+ A reliable hook has explicit input, authority, output, and failure behavior.
10
+
11
+ ## Hook contract
12
+
13
+ ```text
14
+ name:
15
+ event:
16
+ scope:
17
+ input refs:
18
+ handler:
19
+ reads:
20
+ writes/side effects:
21
+ credential(s) + scopes:
22
+ timeout:
23
+ retry/backoff:
24
+ idempotency key:
25
+ output/receipt:
26
+ on failure:
27
+ owner:
28
+ ```
29
+
30
+ ### Input
31
+
32
+ Prefer immutable or content-addressed references where possible: Git SHA, Merkle root,
33
+ deployment ID, schema version, artifact digest, event ID. Do not make the handler infer
34
+ which candidate it should operate on from mutable ambient state.
35
+
36
+ ### Authority
37
+
38
+ Give the hook only the credential scopes it needs. A test job should not hold a
39
+ production deploy token. A postdeploy health checker should not need database-admin
40
+ credentials just to call a public health path.
41
+
42
+ ### Output
43
+
44
+ Emit bounded structured evidence: status, timing, affected resource IDs, hashes,
45
+ versions, and sanitized error categories. Never emit raw secrets, access tokens, full
46
+ request headers, or unbounded logs into a receipt.
47
+
48
+ ### Failure
49
+
50
+ Choose deliberately:
51
+
52
+ - `warn`: record but do not block;
53
+ - `block`: candidate cannot advance;
54
+ - `rollback`: revert/disable when rollback is proven safe;
55
+ - `quarantine`: keep the candidate deployed but unavailable while investigation runs.
56
+
57
+ Do not default every hook to "retry forever." Retries need a bounded count/window and
58
+ idempotency semantics for side effects.
59
+
60
+ ## Ingress versus egress hooks
61
+
62
+ **Ingress** receives something from outside: HTTP request, webhook, auth callback,
63
+ queue event. Validate identity/signature/schema before trusted business logic.
64
+
65
+ **Egress** sends something outward: API call, deploy, notification, database write.
66
+ Validate destination identity/allowlist, credential scope, request contract, timeout,
67
+ and retry/idempotency behavior.
68
+
69
+ This makes operational I/O inspectable as edges in the same graph as code imports.
70
+
71
+ ## Deploy hook composition
72
+
73
+ A robust deploy wrapper behaves like:
74
+
75
+ ```text
76
+ predeploy
77
+ inspect/security/build/tests
78
+ capture evidence
79
+
80
+ provider deploy
81
+ capture deployment/version ID
82
+
83
+ postdeploy
84
+ smoke critical routes/contracts
85
+ inspect metrics if available
86
+
87
+ promote
88
+ success receipt only if postdeploy is green
89
+
90
+ failure
91
+ failure receipt
92
+ rollback/disable/quarantine according to project policy
93
+ ```
94
+
95
+ Do not hide this behind one opaque shell string when individual steps need different
96
+ failure semantics or receipts.
@@ -0,0 +1,53 @@
1
+ {
2
+ "schema_version": 1,
3
+ "skills": [
4
+ {
5
+ "id": "agentsam-jr-dev",
6
+ "aliases": [
7
+ "agentsam_jr_dev",
8
+ "jr-dev"
9
+ ],
10
+ "title": "AgentSam Jr Dev",
11
+ "description": "Teach, inspect, build, and revise real software using beginner-friendly language grounded in repository and runtime evidence.",
12
+ "entry": "agentsam-jr-dev/SKILL.md",
13
+ "references": [
14
+ "agentsam-jr-dev/references/web-application-fundamentals.md",
15
+ "agentsam-jr-dev/references/real-application-logic.md"
16
+ ],
17
+ "user_invocable": true
18
+ },
19
+ {
20
+ "id": "agentsam-app-fundamentals",
21
+ "aliases": [
22
+ "app-fundamentals",
23
+ "application-fundamentals",
24
+ "quick-bytes"
25
+ ],
26
+ "title": "AgentSam Application Fundamentals",
27
+ "description": "Build and review applications using explicit execution ownership, contracts, credentialed destinations, dependency graphs, AST evidence, and Merkle evidence.",
28
+ "entry": "agentsam-app-fundamentals/SKILL.md",
29
+ "references": [
30
+ "agentsam-app-fundamentals/references/trust-credentials-and-destinations.md",
31
+ "agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md"
32
+ ],
33
+ "user_invocable": true
34
+ },
35
+ {
36
+ "id": "agentsam-progression-guard",
37
+ "aliases": [
38
+ "progression-guard",
39
+ "no-regress",
40
+ "ship-guard",
41
+ "release-guard"
42
+ ],
43
+ "title": "AgentSam Progression Guard",
44
+ "description": "Prevent silent regression with explicit local, CI, predeploy, deploy, postdeploy, promotion, and runtime evidence gates.",
45
+ "entry": "agentsam-progression-guard/SKILL.md",
46
+ "references": [
47
+ "agentsam-progression-guard/references/checkpoint-chain.md",
48
+ "agentsam-progression-guard/references/hooks-operational-io.md"
49
+ ],
50
+ "user_invocable": true
51
+ }
52
+ ]
53
+ }
@@ -5,3 +5,10 @@ export {
5
5
  listCapabilities,
6
6
  } from './manifest.js';
7
7
  export { repositorySnapshot } from './repository-snapshot.js';
8
+ export {
9
+ REPOSITORY_SNAPSHOT_VIEWS,
10
+ buildRepositorySnapshotFacets,
11
+ normalizeRepositorySnapshotFilters,
12
+ projectRepositorySnapshot,
13
+ repositoryFileMatches,
14
+ } from './repository-snapshot-view.js';
@@ -0,0 +1,238 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const VIEW_NAMES = new Set(['index', 'files', 'full']);
4
+ const FILTER_FIELDS = Object.freeze([
5
+ 'system', 'package', 'category', 'layer', 'kind', 'language', 'role', 'execution_domain', 'tag', 'path', 'symbol', 'import', 'match',
6
+ ]);
7
+ const DEFAULT_LIMIT = 50;
8
+ const MAX_LIMIT = 500;
9
+ const DEFAULT_FACET_LIMIT = 48;
10
+ const MAX_FACET_LIMIT = 200;
11
+
12
+ function clean(value) {
13
+ return value == null ? '' : String(value).trim();
14
+ }
15
+
16
+ function sha256(value) {
17
+ return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
18
+ }
19
+
20
+ function values(input) {
21
+ if (input == null) return [];
22
+ const raw = Array.isArray(input) ? input : [input];
23
+ return [...new Set(raw.map(clean).filter(Boolean))];
24
+ }
25
+
26
+ function boundedInt(value, fallback, max) {
27
+ const number = Number(value);
28
+ if (!Number.isInteger(number) || number < 1) return fallback;
29
+ return Math.min(max, number);
30
+ }
31
+
32
+ function globRegex(glob) {
33
+ const input = clean(glob).replace(/^\.\//, '');
34
+ let source = '^';
35
+ for (let i = 0; i < input.length; i += 1) {
36
+ const char = input[i];
37
+ if (char === '*') {
38
+ if (input[i + 1] === '*') { source += '.*'; i += 1; }
39
+ else source += '[^/]*';
40
+ } else if (char === '?') source += '[^/]';
41
+ else source += /[\\^$.*+?()[\]{}|]/.test(char) ? `\\${char}` : char;
42
+ }
43
+ return new RegExp(`${source}$`, 'i');
44
+ }
45
+
46
+ function pathMatches(path, selectors) {
47
+ if (!selectors.length) return true;
48
+ const candidate = clean(path).replace(/^\.\//, '');
49
+ return selectors.some((selector) => {
50
+ const normalized = selector.replace(/^\.\//, '');
51
+ if (normalized.includes('*') || normalized.includes('?')) return globRegex(normalized).test(candidate);
52
+ return candidate === normalized || candidate.startsWith(`${normalized.replace(/\/$/, '')}/`);
53
+ });
54
+ }
55
+
56
+ function scalarMatches(value, selectors) {
57
+ if (!selectors.length) return true;
58
+ const candidate = clean(value).toLowerCase();
59
+ return selectors.some((selector) => candidate === selector.toLowerCase());
60
+ }
61
+
62
+ function listMatches(list, selectors) {
63
+ if (!selectors.length) return true;
64
+ const set = new Set((Array.isArray(list) ? list : []).map((value) => clean(value).toLowerCase()));
65
+ return selectors.some((selector) => set.has(selector.toLowerCase()));
66
+ }
67
+
68
+ function lexicalMatches(entry, terms) {
69
+ if (!terms.length) return true;
70
+ const haystack = [
71
+ entry.path, entry.system, entry.package, entry.category, entry.layer, entry.kind, entry.language, entry.role, entry.execution_domain,
72
+ ...(entry.tags || []), ...(entry.symbols || []), ...(entry.imports || []),
73
+ ].map(clean).join('\n').toLowerCase();
74
+ return terms.every((term) => haystack.includes(term.toLowerCase()));
75
+ }
76
+
77
+ export function normalizeRepositorySnapshotFilters(input = {}) {
78
+ return Object.fromEntries(FILTER_FIELDS.map((field) => [field, values(input[field])]).filter(([, list]) => list.length));
79
+ }
80
+
81
+ export function repositoryFileMatches(entry, filters = {}) {
82
+ const f = normalizeRepositorySnapshotFilters(filters);
83
+ return scalarMatches(entry.system, f.system || [])
84
+ && scalarMatches(entry.package, f.package || [])
85
+ && scalarMatches(entry.category, f.category || [])
86
+ && scalarMatches(entry.layer, f.layer || [])
87
+ && scalarMatches(entry.kind, f.kind || [])
88
+ && scalarMatches(entry.language, f.language || [])
89
+ && scalarMatches(entry.role, f.role || [])
90
+ && scalarMatches(entry.execution_domain, f.execution_domain || [])
91
+ && listMatches(entry.tags, f.tag || [])
92
+ && pathMatches(entry.path, f.path || [])
93
+ && listMatches(entry.symbols, f.symbol || [])
94
+ && listMatches(entry.imports, f.import || [])
95
+ && lexicalMatches(entry, f.match || []);
96
+ }
97
+
98
+ function countFacet(entries, getter, limit) {
99
+ const counts = new Map();
100
+ for (const entry of entries) {
101
+ for (const raw of getter(entry)) {
102
+ const value = clean(raw);
103
+ if (!value) continue;
104
+ counts.set(value, (counts.get(value) || 0) + 1);
105
+ }
106
+ }
107
+ return [...counts.entries()]
108
+ .map(([value, count]) => ({ value, count }))
109
+ .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))
110
+ .slice(0, limit);
111
+ }
112
+
113
+ export function buildRepositorySnapshotFacets(entries = [], { limit = DEFAULT_FACET_LIMIT } = {}) {
114
+ const bounded = boundedInt(limit, DEFAULT_FACET_LIMIT, MAX_FACET_LIMIT);
115
+ const scalar = (field) => (entry) => entry[field] == null ? [] : [entry[field]];
116
+ return {
117
+ systems: countFacet(entries, scalar('system'), bounded),
118
+ packages: countFacet(entries, scalar('package'), bounded),
119
+ layers: countFacet(entries, scalar('layer'), bounded),
120
+ roles: countFacet(entries, scalar('role'), bounded),
121
+ execution_domains: countFacet(entries, scalar('execution_domain'), bounded),
122
+ kinds: countFacet(entries, scalar('kind'), bounded),
123
+ languages: countFacet(entries, scalar('language'), bounded),
124
+ tags: countFacet(entries, (entry) => entry.tags || [], bounded),
125
+ categories: countFacet(entries, scalar('category'), bounded),
126
+ };
127
+ }
128
+
129
+ function compactEntry(entry) {
130
+ return {
131
+ path: entry.path,
132
+ ...(entry.package ? { package: entry.package } : {}),
133
+ ...(entry.package_root ? { package_root: entry.package_root } : {}),
134
+ ...(entry.system ? { system: entry.system } : {}),
135
+ ...(entry.category ? { category: entry.category } : {}),
136
+ ...(entry.layer ? { layer: entry.layer } : {}),
137
+ ...(entry.kind ? { kind: entry.kind } : {}),
138
+ ...(entry.language ? { language: entry.language } : {}),
139
+ ...(entry.role ? { role: entry.role } : {}),
140
+ ...(entry.execution_domain ? { execution_domain: entry.execution_domain } : {}),
141
+ ...(entry.tags?.length ? { tags: entry.tags } : {}),
142
+ ...(entry.symbols?.length ? { symbols: entry.symbols } : {}),
143
+ ...(entry.imports?.length ? { imports: entry.imports } : {}),
144
+ };
145
+ }
146
+
147
+ function compactTrustBoundary(analysis, limit = 20) {
148
+ const source = analysis?.trust_boundary;
149
+ if (!source) return undefined;
150
+ const findings = Array.isArray(source.findings) ? source.findings : [];
151
+ return {
152
+ trust_boundary: {
153
+ schema_version: source.schema_version,
154
+ analyzer: source.analyzer,
155
+ metadata_root: source.metadata_root,
156
+ complete: source.complete,
157
+ status: source.status,
158
+ ok: source.ok,
159
+ browser_roots: source.browser_roots,
160
+ browser_reachable_files: source.browser_reachable_files,
161
+ ast_files: source.ast_files,
162
+ finding_count: findings.length,
163
+ findings_returned: Math.min(findings.length, limit),
164
+ findings_truncated: findings.length > limit,
165
+ findings: findings.slice(0, limit).map((item) => ({
166
+ kind: item.kind,
167
+ severity: item.severity,
168
+ source: item.source,
169
+ ...(item.target ? { target: item.target } : {}),
170
+ ...(item.specifier ? { specifier: item.specifier } : {}),
171
+ ...(item.env ? { env: item.env } : {}),
172
+ ...(item.repair?.action ? { repair_action: item.repair.action } : {}),
173
+ })),
174
+ },
175
+ };
176
+ }
177
+
178
+ function projectionBase(snapshot) {
179
+ return {
180
+ schema_version: snapshot.schema_version,
181
+ capability: snapshot.capability,
182
+ snapshot_id: snapshot.snapshot_id,
183
+ created_at: snapshot.created_at,
184
+ content_hash: snapshot.content_hash,
185
+ repository: snapshot.repository,
186
+ tree: {
187
+ merkle_root: snapshot.tree?.merkle_root || null,
188
+ metadata_root: snapshot.tree?.metadata_root || null,
189
+ manifest: snapshot.tree?.manifest || null,
190
+ classifier: snapshot.tree?.classifier || null,
191
+ stats: snapshot.tree?.stats || null,
192
+ semantic_stats: snapshot.tree?.semantic_stats || null,
193
+ },
194
+ ...(compactTrustBoundary(snapshot.analysis) ? { analysis: compactTrustBoundary(snapshot.analysis) } : {}),
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Produce a bounded, deterministic view over a canonical repository.snapshot.
200
+ * The canonical content_hash / Merkle identities are carried through unchanged;
201
+ * projection_hash identifies only this derived view and is never an authority root.
202
+ */
203
+ export function projectRepositorySnapshot(snapshot, options = {}) {
204
+ if (!snapshot || snapshot.capability !== 'repository.snapshot' || !snapshot.tree) {
205
+ throw new Error('repository_snapshot_required');
206
+ }
207
+ const view = clean(options.view || 'index').toLowerCase();
208
+ if (!VIEW_NAMES.has(view)) throw new Error(`repository_snapshot_view_invalid:${view}`);
209
+ if (view === 'full') return snapshot;
210
+
211
+ const files = Array.isArray(snapshot.tree.files) ? snapshot.tree.files : [];
212
+ const filters = normalizeRepositorySnapshotFilters(options.filters || options);
213
+ const filtered = files.filter((entry) => repositoryFileMatches(entry, filters));
214
+ const limit = boundedInt(options.limit, DEFAULT_LIMIT, MAX_LIMIT);
215
+ const facetLimit = boundedInt(options.facetLimit ?? options.facet_limit, DEFAULT_FACET_LIMIT, MAX_FACET_LIMIT);
216
+ const base = projectionBase(snapshot);
217
+ const result = {
218
+ ...base,
219
+ projection: {
220
+ format: 'agentsam-repository-view',
221
+ version: 1,
222
+ view,
223
+ source_snapshot_id: snapshot.snapshot_id,
224
+ source_content_hash: snapshot.content_hash,
225
+ filters,
226
+ matched: filtered.length,
227
+ returned: view === 'files' ? Math.min(filtered.length, limit) : 0,
228
+ truncated: view === 'files' && filtered.length > limit,
229
+ limit: view === 'files' ? limit : 0,
230
+ },
231
+ facets: buildRepositorySnapshotFacets(filtered, { limit: facetLimit }),
232
+ ...(view === 'files' ? { files: filtered.slice(0, limit).map(compactEntry) } : {}),
233
+ };
234
+ result.projection.projection_hash = sha256({ ...result, projection: { ...result.projection, projection_hash: undefined } });
235
+ return result;
236
+ }
237
+
238
+ export const REPOSITORY_SNAPSHOT_VIEWS = Object.freeze([...VIEW_NAMES]);
@@ -5,7 +5,10 @@ import { promisify } from 'node:util';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { resolveGitContext } from '../lib/git-context.js';
8
+ import { getRepositoryId, tryReadProjectConfig } from '../lib/project-config.js';
8
9
  import { buildMerkleTree } from '../lib/merkle/index.js';
10
+ import { gitIgnoredPaths } from '../lib/merkle/git-ignore.js';
11
+ import { analyzeExecutionBoundaries } from '../indexing/execution-boundary.js';
9
12
  import { showLatestDeployReceipt } from '../lib/deploy-receipt/index.js';
10
13
  import { CONFIG_PATH, canonical, readConfig, scopeKey } from '../knowledge/config.js';
11
14
  import { openSqliteStore } from '../knowledge/stores/sqlite.js';
@@ -24,16 +27,6 @@ function providerForHost(host) {
24
27
  return value ? 'git' : null;
25
28
  }
26
29
 
27
- async function gitIgnoredPaths(root) {
28
- const result = await execute('git', ['status', '--porcelain=v1', '--ignored=matching'], {
29
- cwd: root,
30
- maxBuffer: 16 * 1024 * 1024,
31
- });
32
- return [...new Set(result.stdout.split('\n')
33
- .filter((line) => line.startsWith('!! '))
34
- .map((line) => line.slice(3).trim().replace(/\/$/, ''))
35
- .filter((value) => value && !value.includes('\\') && !value.split('/').includes('..')))];
36
- }
37
30
 
38
31
  async function runRepositoryIntelligence(root, churnDays) {
39
32
  const pythonRoot = fileURLToPath(new URL('../../python', import.meta.url));
@@ -121,21 +114,29 @@ export async function repositorySnapshot({ cwd = process.cwd(), churnDays = 30 }
121
114
  const git = resolveGitContext({ cwd });
122
115
  const root = git.root;
123
116
  const ignored = await gitIgnoredPaths(root);
117
+ const projectConfig = tryReadProjectConfig(root);
124
118
  const [intelligence, merkle, knowledge, deployReceipt] = await Promise.all([
125
119
  runRepositoryIntelligence(root, churnDays),
126
- buildMerkleTree(root, { exclude: ignored }),
120
+ buildMerkleTree(root, { exclude: ignored, semantic: true }),
127
121
  readKnowledgeState(root),
128
122
  showLatestDeployReceipt({ root }),
129
123
  ]);
130
124
  const provider = providerForHost(git.remoteHost);
131
- const portableRepositoryId = provider && git.repoFullName ? `${provider}:${git.repoFullName}` : null;
132
- const repositoryId = knowledge.repository_id || portableRepositoryId;
125
+ const portableRepositoryId = provider && git.repoFullName
126
+ ? `${provider}:${provider === 'github' ? git.repoFullName.toLowerCase() : git.repoFullName}`
127
+ : null;
128
+ const projectRepositoryId = getRepositoryId(projectConfig);
129
+ if (projectRepositoryId && knowledge.repository_id && projectRepositoryId !== knowledge.repository_id) {
130
+ throw new Error(`repository_identity_mismatch: project=${projectRepositoryId} knowledge=${knowledge.repository_id}`);
131
+ }
132
+ const repositoryId = projectRepositoryId || knowledge.repository_id || portableRepositoryId;
133
133
  const packages = readPackageInventory(root, intelligence.manifests || []);
134
+ const trustBoundary = analyzeExecutionBoundaries(merkle.semantic);
134
135
 
135
136
  const evidence = {
136
137
  repository: {
137
138
  repository_id: repositoryId,
138
- identity_source: knowledge.repository_id ? 'knowledge-config' : portableRepositoryId ? 'git-remote' : 'unresolved',
139
+ identity_source: projectRepositoryId ? 'project-config' : knowledge.repository_id ? 'knowledge-config' : portableRepositoryId ? 'git-remote' : 'unresolved',
139
140
  provider,
140
141
  full_name: git.repoFullName,
141
142
  remote_url: git.remoteUrl || null,
@@ -145,10 +146,20 @@ export async function repositorySnapshot({ cwd = process.cwd(), churnDays = 30 }
145
146
  },
146
147
  tree: {
147
148
  merkle_root: merkle.rootHash,
149
+ metadata_root: merkle.semantic.rootHash,
150
+ manifest: {
151
+ format: merkle.format,
152
+ version: merkle.version,
153
+ hash_algorithm: merkle.algorithm,
154
+ policy_hash: merkle.policyHash,
155
+ },
156
+ classifier: merkle.semantic.classifier,
148
157
  stats: merkle.stats,
158
+ semantic_stats: merkle.semantic.stats,
149
159
  paths: merkle.entries
150
160
  .filter((entry) => entry.type === 'file' || entry.type === 'symlink')
151
161
  .map((entry) => entry.path),
162
+ files: merkle.semantic.entries,
152
163
  },
153
164
  intelligence: {
154
165
  summary: intelligence.summary,
@@ -159,6 +170,9 @@ export async function repositorySnapshot({ cwd = process.cwd(), churnDays = 30 }
159
170
  },
160
171
  packages,
161
172
  knowledge,
173
+ analysis: {
174
+ trust_boundary: trustBoundary,
175
+ },
162
176
  deploy: deployReceipt ? {
163
177
  status: deployReceipt.status,
164
178
  root_hash: deployReceipt.root_hash,