@dzhechkov/harness-core 0.8.28 → 0.8.29

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 (47) hide show
  1. package/.dz-manifest.json +91 -31
  2. package/README.md +15 -2
  3. package/dist/amendment-trace.d.ts.map +1 -1
  4. package/dist/amendment-trace.js +10 -1
  5. package/dist/amendment-trace.js.map +1 -1
  6. package/dist/cmd-usage.d.ts.map +1 -1
  7. package/dist/cmd-usage.js +22 -10
  8. package/dist/cmd-usage.js.map +1 -1
  9. package/dist/core-boundary.d.ts +1 -1
  10. package/dist/core-boundary.d.ts.map +1 -1
  11. package/dist/core-boundary.js +8 -1
  12. package/dist/core-boundary.js.map +1 -1
  13. package/dist/guard.d.ts +19 -0
  14. package/dist/guard.d.ts.map +1 -1
  15. package/dist/guard.js +110 -0
  16. package/dist/guard.js.map +1 -1
  17. package/dist/index.d.ts +5 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/publish.d.ts +9 -0
  22. package/dist/publish.d.ts.map +1 -1
  23. package/dist/publish.js +82 -0
  24. package/dist/publish.js.map +1 -1
  25. package/dist/release-line.d.ts +10 -0
  26. package/dist/release-line.d.ts.map +1 -0
  27. package/dist/release-line.js +21 -0
  28. package/dist/release-line.js.map +1 -0
  29. package/dist/repo-boundary.d.ts +13 -0
  30. package/dist/repo-boundary.d.ts.map +1 -0
  31. package/dist/repo-boundary.js +16 -0
  32. package/dist/repo-boundary.js.map +1 -0
  33. package/dist/repository-origin.d.ts +2 -0
  34. package/dist/repository-origin.d.ts.map +1 -0
  35. package/dist/repository-origin.js +2 -0
  36. package/dist/repository-origin.js.map +1 -0
  37. package/package.json +6 -6
  38. package/sbom.json +180 -30
  39. package/src/amendment-trace.ts +10 -1
  40. package/src/cmd-usage.ts +13 -6
  41. package/src/core-boundary.ts +7 -1
  42. package/src/guard.ts +137 -1
  43. package/src/index.ts +6 -1
  44. package/src/publish.ts +84 -0
  45. package/src/release-line.ts +31 -0
  46. package/src/repo-boundary.ts +25 -0
  47. package/src/repository-origin.ts +1 -0
package/src/guard.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  type VolumeShadowInput,
22
22
  type VolumeShadowResult,
23
23
  } from './guard-volume.js';
24
+ import { findReleaseLine } from './release-line.js';
24
25
 
25
26
  export type GuardSeverity = 'hard' | 'soft';
26
27
  export type GuardOp = 'publish' | 'teach' | 'consolidate' | 'reindex';
@@ -79,6 +80,21 @@ export interface GuardResult {
79
80
  /** Facts the CLI injects; each rule reads only the fields it needs. Missing evidence ⇒ that rule is skipped. */
80
81
  export interface GuardFacts {
81
82
  readonly op: GuardOp;
83
+ /** Signature evidence gathered by the CLI. The pure evaluator never reads manifests or keys. */
84
+ readonly signedPacks?: readonly {
85
+ readonly name: string;
86
+ readonly dir: string;
87
+ readonly changed: boolean;
88
+ readonly ok: boolean | null;
89
+ readonly failures: readonly string[];
90
+ readonly note?: string;
91
+ }[];
92
+ /** Release-line evidence gathered by the CLI; absence means the rule was not established. */
93
+ readonly releaseLines?: {
94
+ readonly readmes: readonly { readonly path: string; readonly text: string | null }[];
95
+ readonly coreVersion: string | null;
96
+ readonly cliVersion: string | null;
97
+ };
82
98
  /** Publish-only raw volume facts. Absence preserves the legacy result shape. */
83
99
  readonly volume?: VolumeShadowInput;
84
100
  /** for no-workspace-star: each publishable package's deps map. */
@@ -165,7 +181,7 @@ export interface GuardFacts {
165
181
  /** for skills-registrable: per skill pack, dirs that would ship un-registrable (no depth-1 SKILL.md). */
166
182
  readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
167
183
  /** for readme-first: per publishable package, is a version bump staged without a README change? */
168
- readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean }[];
184
+ readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean; readonly versionUnknown?: boolean }[];
169
185
  /**
170
186
  * for review-round: per publishable package, does this change bump a version AND touch SOURCE, and
171
187
  * did it bring a GRADED QE report with it? `undefined` (the whole fact absent) means the tree could
@@ -347,6 +363,99 @@ function unquoteYaml(s: string): string {
347
363
  return t;
348
364
  }
349
365
 
366
+ function inspectReleaseLines(
367
+ evidence: NonNullable<GuardFacts['releaseLines']>,
368
+ severity: GuardSeverity,
369
+ ): {
370
+ readonly violations: Violation[];
371
+ readonly observations: GuardObservation[];
372
+ } {
373
+ const expected = evidence.coreVersion !== null && evidence.cliVersion !== null
374
+ ? { core: evidence.coreVersion, cli: evidence.cliVersion }
375
+ : null;
376
+ const violations: Violation[] = [];
377
+ const observations: GuardObservation[] = [];
378
+
379
+ for (const readme of evidence.readmes) {
380
+ if (readme.text === null) {
381
+ observations.push({
382
+ schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
383
+ metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
384
+ unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
385
+ detail: 'файл не прочитан',
386
+ });
387
+ continue;
388
+ }
389
+ const found = findReleaseLine(readme.text);
390
+ if (found === null) {
391
+ observations.push({
392
+ schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
393
+ metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
394
+ unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
395
+ detail: 'строка релиза не найдена',
396
+ });
397
+ continue;
398
+ }
399
+ if (expected === null) {
400
+ const missing = [
401
+ ...(evidence.coreVersion === null ? ['harness-core package.json version'] : []),
402
+ ...(evidence.cliVersion === null ? ['harness-cli package.json version'] : []),
403
+ ];
404
+ observations.push({
405
+ schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
406
+ metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: [found.core, found.cli],
407
+ unit: 'artifact_set', signal: false, operands: { actual: [found.core, found.cli] },
408
+ method: 'release-line-regex/v1', detail: `${missing.join(' и ')} не прочитана или не имеет форму N.N.N`,
409
+ });
410
+ continue;
411
+ }
412
+ const mismatch = found.core !== expected.core || found.cli !== expected.cli;
413
+ const detail = mismatch
414
+ ? `${readme.path}: строка релиза говорит core v${found.core}/cli v${found.cli}, package.json — v${expected.core}/v${expected.cli}`
415
+ : `${readme.path}: строка релиза совпадает с package.json (${expected.core}/${expected.cli})`;
416
+ observations.push({
417
+ schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
418
+ metric: 'release-line-version-sync', scope: readme.path,
419
+ status: mismatch ? 'outside-reference' : 'within-reference', value: [found.core, found.cli],
420
+ unit: 'artifact_set', signal: mismatch,
421
+ operands: { actual: [found.core, found.cli], expected: [expected.core, expected.cli] },
422
+ method: 'release-line-regex/v1', detail,
423
+ });
424
+ if (mismatch) violations.push({ rule: 'release-line-in-sync', severity, detail });
425
+ }
426
+ return { violations, observations };
427
+ }
428
+
429
+ function inspectSignatureFresh(
430
+ evidence: NonNullable<GuardFacts['signedPacks']>,
431
+ severity: GuardSeverity,
432
+ ): {
433
+ readonly violations: Violation[];
434
+ readonly observations: GuardObservation[];
435
+ } {
436
+ const violations: Violation[] = [];
437
+ const observations: GuardObservation[] = [];
438
+ for (const pack of evidence) {
439
+ if (!pack.changed) continue;
440
+ if (pack.ok === false) {
441
+ const firstFailure = pack.failures[0] ?? 'verification failed without a named reason';
442
+ violations.push({
443
+ rule: 'signature-fresh',
444
+ severity,
445
+ detail: `${pack.name}: files changed but the signed manifest is stale (${firstFailure}) — re-sign: dz sign --pack ${pack.dir} --key <key>`,
446
+ });
447
+ } else if (pack.ok === true) {
448
+ observations.push({
449
+ schemaVersion: 'volume-shadow/v1', rule: 'signature-fresh' as never,
450
+ metric: 'signed-manifest-freshness', scope: pack.name, status: 'within-reference', value: 1,
451
+ unit: 'artifact_set', signal: false, operands: { changed: 1, verified: 1 },
452
+ method: 'verify-manifest/v1', detail: `${pack.name}: ${pack.note ?? 'changed files still match its signed manifest'}`,
453
+ });
454
+ }
455
+ }
456
+ return { violations, observations };
457
+ }
458
+
350
459
  /** The built-in rule set (works with no config). Ops are the mutating operations each rule guards. */
351
460
  export const DEFAULT_RULES: readonly GuardRule[] = [
352
461
  { id: 'no-workspace-star', severity: 'hard', ops: ['publish'], description: 'a published package.json must carry no workspace:* dep (npm ships it verbatim → the install breaks)' },
@@ -356,6 +465,8 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
356
465
  { id: 'backlog-covers-features', severity: 'soft', ops: ['publish', 'consolidate'], description: 'каталог фичи, заведённый после базовой даты, назван записью бэклога — либо несёт именованную оговорку с причиной' },
357
466
  { id: 'no-secrets', severity: 'hard', ops: ['teach', 'publish'], description: 'no private key or API token in lesson text or a published file' },
358
467
  { id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
468
+ { id: 'release-line-in-sync', severity: 'soft', ops: ['publish'], description: 'root and harness-cli README release lines agree with the harness-core and harness-cli package versions' },
469
+ { id: 'signature-fresh', severity: 'soft', ops: ['publish'], description: 'a pack whose files changed in this diff still verifies against its signed .dz-manifest.json — a stale signature is named before publish, not at the gate' },
359
470
  { id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
360
471
  { id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
361
472
  { id: 'routing-store-stale', severity: 'soft', ops: ['publish'], description: 'harvested routing telemetry has been applied to the auto-cost outcome store' },
@@ -921,6 +1032,8 @@ const HAS_INPUT: Partial<Record<string, (f: GuardFacts) => boolean>> = {
921
1032
  // Дерево без плагин-манифестов правилу нечего сказать: «прошло» тут значило бы «не смотрели».
922
1033
  'plugin-manifest-audit': (f) => Array.isArray(f.pluginManifests) && f.pluginManifests.length > 0,
923
1034
  'no-secrets': (f) => Array.isArray(f.secretTargets) && f.secretTargets.length > 0,
1035
+ 'release-line-in-sync': (f) => typeof f.releaseLines === 'object' && f.releaseLines !== null,
1036
+ 'signature-fresh': (f) => Array.isArray(f.signedPacks),
924
1037
  };
925
1038
 
926
1039
  /**
@@ -1065,6 +1178,29 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
1065
1178
  continue;
1066
1179
  }
1067
1180
  checked.push(r.id);
1181
+ if (r.id === 'readme-first') {
1182
+ for (const p of facts.readmeFirst ?? []) {
1183
+ if (p?.versionUnknown !== true) continue;
1184
+ observations.push({
1185
+ schemaVersion: 'volume-shadow/v1', rule: 'readme-first' as never,
1186
+ metric: 'package-version-changed-from-head', scope: p.name, status: 'unknown', value: null,
1187
+ unit: 'artifact_set', signal: false, operands: {}, method: 'git-show-head-package-version/v1',
1188
+ detail: `${p.name}: package version could not be compared with HEAD; readme-first stayed advisory and emitted no violation`,
1189
+ });
1190
+ }
1191
+ }
1192
+ if (r.id === 'release-line-in-sync') {
1193
+ const inspection = inspectReleaseLines(facts.releaseLines!, r.severity);
1194
+ observations.push(...inspection.observations);
1195
+ violations.push(...inspection.violations);
1196
+ continue;
1197
+ }
1198
+ if (r.id === 'signature-fresh') {
1199
+ const inspection = inspectSignatureFresh(facts.signedPacks!, r.severity);
1200
+ observations.push(...inspection.observations);
1201
+ violations.push(...inspection.violations);
1202
+ continue;
1203
+ }
1068
1204
  if ((VOLUME_SHADOW_RULE_IDS as readonly string[]).includes(r.id)) {
1069
1205
  const emission = volume();
1070
1206
  observations.push(...emission.observations.filter((item) => item.rule === r.id));
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ import { createRequire } from 'node:module';
10
10
  export const HARNESS_CORE_VERSION: string =
11
11
  (createRequire(import.meta.url)('../package.json') as { version: string }).version;
12
12
 
13
+ export { REPOSITORY_ORIGIN } from './repository-origin.js';
14
+
13
15
  export * from './skills.js';
14
16
  export * from './apply.js';
15
17
  export {
@@ -123,6 +125,8 @@ export { planLedgerBackfill, LEDGER_FILL_SOURCE, AMBIGUOUS, resolveLedgerRunId }
123
125
  // project-skills root resolution (field report doc-25b): the ONE builder behind both the Step-0
124
126
  // probe and the PS_GUIDANCE paragraph, so the two can never look at different roots again.
125
127
  export { projectSkillsOneRoot, projectSkillsProbeCommand } from './project-skills-root.js';
128
+ export { isRepoBoundary } from './repo-boundary.js';
129
+ export type { RepoBoundaryIo } from './repo-boundary.js';
126
130
  export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
127
131
  export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
128
132
  export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
@@ -660,7 +664,8 @@ export type {
660
664
  ChainDefectAges,
661
665
  ChainedJournal,
662
666
  } from './event-chain.js';
663
- export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
667
+ export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
668
+ export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
664
669
  export * from './course-staleness.js';
665
670
  export { fetchAllDownloads } from './downloads.js';
666
671
  export type { PackageDownloads, DownloadsReport } from './downloads.js';
package/src/publish.ts CHANGED
@@ -16,6 +16,7 @@ import { execSync } from 'node:child_process';
16
16
  type ExecSyncOptionsWithStringEncoding = NonNullable<Parameters<typeof execSync>[1]> & { encoding: 'utf-8' };
17
17
 
18
18
  import { claimCheck } from './claim-check.js';
19
+ import { rewriteReleaseLine } from './release-line.js';
19
20
 
20
21
  export type ProbeOutcome = {
21
22
  readonly attempt: number;
@@ -67,6 +68,10 @@ export interface PublishReport {
67
68
  readonly skipped: number;
68
69
  readonly errors: number;
69
70
  readonly dryRun: boolean;
71
+ /** Repo-relative README paths whose first joint core/CLI release line was rewritten. */
72
+ readonly releaseLineSynced: readonly string[];
73
+ /** Post-publication sync failures are warnings: registry-confirmed packages cannot be unpublished. */
74
+ readonly warnings?: readonly string[] | undefined;
70
75
  }
71
76
 
72
77
  /** Is `p` inside `dir`? Used to refuse a signing key that lives in the repository working tree. */
@@ -130,6 +135,32 @@ function maxPublished(name: string, localVersion: string, exec: PublishExec = ex
130
135
  // the publish itself succeeds, and every consumer `npm install` then fails with ETARGET. Staged is
131
136
  // not shipped; this preflight makes the difference a refusal instead of a broken release.
132
137
 
138
+ /**
139
+ * Mirror pnpm's package-time expansion of the three shorthand workspace dependency specs.
140
+ * Pure by construction: callers provide both the source bytes and the sibling version table.
141
+ */
142
+ export function rewriteWorkspaceSpecs(
143
+ pkgJsonText: string,
144
+ siblingVersions: ReadonlyMap<string, string>,
145
+ ): string {
146
+ const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
147
+ const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
148
+ for (const field of fields) {
149
+ const candidate = pkg[field];
150
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
151
+ const table = candidate as Record<string, unknown>;
152
+ for (const [dep, spec] of Object.entries(table)) {
153
+ if (typeof spec !== 'string') continue;
154
+ const match = /^workspace:([*^~])$/.exec(spec);
155
+ const version = siblingVersions.get(dep);
156
+ if (match === null || version === undefined) continue;
157
+ const marker = match[1]!;
158
+ table[dep] = marker === '*' ? version : `${marker}${version}`;
159
+ }
160
+ }
161
+ return JSON.stringify(pkg, null, 2) + '\n';
162
+ }
163
+
133
164
  /**
134
165
  * Pure half: which `workspace:`-declared deps of a package would pack to a floor that is neither
135
166
  * being published in this batch nor already on the registry?
@@ -892,11 +923,64 @@ export function publishPackages(
892
923
  }
893
924
  }
894
925
 
926
+ const releaseLineSynced: string[] = [];
927
+ const warnings: string[] = [];
928
+ const releasePackageNames = new Set(['@dzhechkov/harness-core', '@dzhechkov/harness-cli']);
929
+ const releasePackagePublished = results.some(
930
+ (result) => result.status === 'published' && releasePackageNames.has(result.name),
931
+ );
932
+ if (releasePackagePublished && opts.dryRun !== true && opts.bumpOnly !== true) {
933
+ const currentVersion = (name: string): string | null => {
934
+ const landed = results.find((result) => result.name === name && result.status === 'published');
935
+ if (landed !== undefined) return landed.newVersion;
936
+ const pkg = packages.find((candidate) => candidate.name === name);
937
+ if (pkg === undefined) return null;
938
+ try {
939
+ const parsed = JSON.parse(readFileSync(pathJoin(pkg.dir, 'package.json'), 'utf8')) as { version?: unknown };
940
+ return typeof parsed.version === 'string' ? parsed.version : null;
941
+ } catch {
942
+ return null;
943
+ }
944
+ };
945
+ const coreVersion = currentVersion('@dzhechkov/harness-core');
946
+ const cliVersion = currentVersion('@dzhechkov/harness-cli');
947
+ if (coreVersion === null || cliVersion === null) {
948
+ warnings.push('release-line sync skipped: could not read both harness-core and harness-cli package versions');
949
+ } else {
950
+ const readmes = [
951
+ { path: 'README.md', absolute: pathJoin(monorepoRoot, 'README.md') },
952
+ {
953
+ path: 'packages/@dzhechkov/harness-cli/README.md',
954
+ absolute: pathJoin(monorepoRoot, 'packages', '@dzhechkov', 'harness-cli', 'README.md'),
955
+ },
956
+ ] as const;
957
+ for (const readme of readmes) {
958
+ try {
959
+ const original = readFileSync(readme.absolute, 'utf8');
960
+ const updated = rewriteReleaseLine(original, coreVersion, cliVersion);
961
+ if (updated === null) {
962
+ warnings.push(`release-line sync skipped ${readme.path}: release line not found`);
963
+ continue;
964
+ }
965
+ if (updated === original) continue;
966
+ const tmp = readme.absolute + '.sync-tmp';
967
+ writeFileSync(tmp, updated);
968
+ renameSync(tmp, readme.absolute);
969
+ releaseLineSynced.push(readme.path);
970
+ } catch (error) {
971
+ warnings.push(`release-line sync failed ${readme.path}: ${error instanceof Error ? error.message : String(error)}`);
972
+ }
973
+ }
974
+ }
975
+ }
976
+
895
977
  return {
896
978
  packages: results,
897
979
  published: results.filter((r) => r.status === 'published').length,
898
980
  skipped: results.filter((r) => r.status === 'skipped').length,
899
981
  errors: results.filter((r) => r.status === 'error').length,
900
982
  dryRun: opts.dryRun === true,
983
+ releaseLineSynced,
984
+ ...(warnings.length > 0 ? { warnings } : {}),
901
985
  };
902
986
  }
@@ -0,0 +1,31 @@
1
+ export const RELEASE_LINE_RE = /`harness-core v(\d+\.\d+\.\d+)` · `harness-cli v(\d+\.\d+\.\d+)`/;
2
+
3
+ export interface ReleaseLineMatch {
4
+ readonly index: number;
5
+ readonly line: string;
6
+ readonly core: string;
7
+ readonly cli: string;
8
+ }
9
+
10
+ export function findReleaseLine(text: string): ReleaseLineMatch | null {
11
+ const lines = text.split('\n');
12
+ for (let index = 0; index < lines.length; index++) {
13
+ const line = lines[index]!;
14
+ const match = RELEASE_LINE_RE.exec(line);
15
+ if (match?.[1] !== undefined && match[2] !== undefined) {
16
+ return { index, line, core: match[1], cli: match[2] };
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export function rewriteReleaseLine(text: string, core: string, cli: string): string | null {
23
+ const found = findReleaseLine(text);
24
+ if (found === null) return null;
25
+ const lines = text.split('\n');
26
+ lines[found.index] = found.line.replace(
27
+ RELEASE_LINE_RE,
28
+ `\`harness-core v${core}\` · \`harness-cli v${cli}\``,
29
+ );
30
+ return lines.join('\n');
31
+ }
@@ -0,0 +1,25 @@
1
+ /** Filesystem facts required to recognise a real Git repository boundary. */
2
+ export interface RepoBoundaryIo {
3
+ exists(p: string): boolean;
4
+ isDirectory(p: string): boolean;
5
+ readText(p: string): string | null;
6
+ }
7
+
8
+ /**
9
+ * A Git repository boundary is either a `.git` directory containing `HEAD`, or a worktree-style
10
+ * `.git` file whose first bytes are the `gitdir:` redirect. An empty or unrelated `.git` entry is
11
+ * not a boundary.
12
+ */
13
+ export function isRepoBoundary(
14
+ dir: string,
15
+ io: RepoBoundaryIo,
16
+ join: (...p: string[]) => string,
17
+ ): boolean {
18
+ const git = join(dir, '.git');
19
+ if (!io.exists(git)) return false;
20
+ if (io.isDirectory(git)) {
21
+ const head = join(git, 'HEAD');
22
+ return io.exists(head) && !io.isDirectory(head);
23
+ }
24
+ return io.readText(git)?.startsWith('gitdir:') === true;
25
+ }
@@ -0,0 +1 @@
1
+ export const REPOSITORY_ORIGIN = 'git+https://github.com/djd1m/dz-harness-hub.git';